diff --git a/.github/workflows/agent.yml b/.github/workflows/agent.yml new file mode 100644 index 000000000..f9cbae52a --- /dev/null +++ b/.github/workflows/agent.yml @@ -0,0 +1,80 @@ +name: orb-agent + +on: + workflow_dispatch: + inputs: + pktvisor_tag: + description: 'pktvisor agent docker tag to package' + required: true + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.16 + + - name: Build orb agent (go build only) + run: make agent_bin + + package: + needs: build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Get branch name + shell: bash + run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/} | tr / -)" >> $GITHUB_ENV + + - name: Debug branch name + run: echo ${{ env.BRANCH_NAME }} + + - name: Generate ref tag (main) + if: ${{ env.BRANCH_NAME == 'main' }} + run: | + echo "REF_TAG=latest" >> $GITHUB_ENV + + - name: Generate ref tag (develop) + if: ${{ env.BRANCH_NAME == 'develop' }} + run: | + echo "REF_TAG=develop" >> $GITHUB_ENV + + - name: Debug ref tag + run: echo ${{ env.REF_TAG }} + + - name: Get VERSION + run: | + echo "VERSION=`cat ${{github.workspace}}/VERSION`" >> $GITHUB_ENV + + - name: Debug version + run: echo ${{ env.VERSION }} + + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build orb-agent + shell: bash + env: + IMAGE_NAME: ns1labs/orb-agent + run: | + if [ "${{ github.event.inputs.pktvisor_tag }}" == "" ]; then + make agent + make agent_debug + else + PKTVISOR_TAG=${{ github.event.inputs.pktvisor_tag }} make agent + PKTVISOR_TAG=${{ github.event.inputs.pktvisor_tag }} make agent_debug + fi + + - name: Push agent container + run: | + docker push -a ns1labs/orb-agent diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index a79f39a2b..10ed5b4c5 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -134,8 +134,10 @@ jobs: run: | if [ "${{ github.event.inputs.pktvisor_tag }}" == "" ]; then make agent + make agent_debug else PKTVISOR_TAG=${{ github.event.inputs.pktvisor_tag }} make agent + PKTVISOR_TAG=${{ github.event.inputs.pktvisor_tag }} make agent_debug fi - name: Build orb-ui diff --git a/Makefile b/Makefile index d6cb3051e..3df98081e 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,9 @@ # expects to be set as env var REF_TAG ?= develop +DEBUG_REF_TAG ?= develop-debug PKTVISOR_TAG ?= latest-develop +PKTVISOR_DEBUG_TAG ?= latest-develop-debug DOCKER_IMAGE_NAME_PREFIX ?= orb DOCKERHUB_REPO = ns1labs BUILD_DIR = build @@ -110,6 +112,12 @@ agent: --tag=$(DOCKERHUB_REPO)/$(DOCKER_IMAGE_NAME_PREFIX)-agent:$(ORB_VERSION)-$(COMMIT_HASH) \ -f agent/docker/Dockerfile . +agent_debug: + docker build \ + --build-arg PKTVISOR_TAG=$(PKTVISOR_DEBUG_TAG) \ + --tag=$(DOCKERHUB_REPO)/$(DOCKER_IMAGE_NAME_PREFIX)-agent:$(DEBUG_REF_TAG) \ + -f agent/docker/Dockerfile . + ui: cd ui/ && docker build \ --build-arg ENV_PS_SID=${PS_SID} \ diff --git a/VERSION b/VERSION index 142464bf2..d33c3a212 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.11.0 \ No newline at end of file +0.12.0 \ No newline at end of file diff --git a/agent/agent.go b/agent/agent.go index e00ff684c..f3678b9dd 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -26,6 +26,7 @@ var ( type Agent interface { Start() error Stop() + Restart(fullReset bool, reason string) } type orbAgent struct { @@ -47,11 +48,16 @@ type orbAgent struct { logTopic string // AgentGroup channels sent from core - groupChannels []string + groupsInfos map[string]GroupInfo policyManager manager.PolicyManager } +type GroupInfo struct { + Name string + ChannelID string +} + var _ Agent = (*orbAgent)(nil) func New(logger *zap.Logger, c config.Config) (Agent, error) { @@ -65,7 +71,7 @@ func New(logger *zap.Logger, c config.Config) (Agent, error) { if err != nil { return nil, err } - return &orbAgent{logger: logger, config: c, policyManager: pm, db: db}, nil + return &orbAgent{logger: logger, config: c, policyManager: pm, db: db, groupsInfos: make(map[string]GroupInfo)}, nil } func (a *orbAgent) startBackends() error { @@ -138,3 +144,24 @@ func (a *orbAgent) Stop() { } a.client.Disconnect(250) } + +func (a *orbAgent) Restart(fullReset bool, reason string) { + if fullReset { + a.logger.Info("restarting all backends", zap.String("reason", reason)) + for name, be := range a.backends { + a.logger.Info("removing policies", zap.String("backend", name)) + if err := a.policyManager.RemoveBackendPolicies(be); err != nil { + a.logger.Error("failed to remove policies", zap.String("backend", name), zap.Error(err)) + } + a.logger.Info("resetting backend", zap.String("backend",name)) + if err := be.FullReset(); err != nil { + a.logger.Error("failed to reset backend", zap.String("backend", name), zap.Error(err)) + } + a.logger.Info("reapplying policies", zap.String("backend", name)) + if err := a.policyManager.ApplyBackendPolicies(be); err != nil { + a.logger.Error("failed to reapply policies", zap.String("backend", name), zap.Error(err)) + } + } + a.logger.Info("all backends were restarted") + } +} \ No newline at end of file diff --git a/agent/backend/backend.go b/agent/backend/backend.go index 9530c1259..76b2de21b 100644 --- a/agent/backend/backend.go +++ b/agent/backend/backend.go @@ -43,11 +43,12 @@ type Backend interface { Version() (string, error) Start() error Stop() error + FullReset() error GetCapabilities() (map[string]interface{}, error) GetState() (BackendState, string, error) - ApplyPolicy(data policies.PolicyData) error + ApplyPolicy(data policies.PolicyData, updatePolicy bool) error RemovePolicy(data policies.PolicyData) error } diff --git a/agent/backend/pktvisor/pktvisor.go b/agent/backend/pktvisor/pktvisor.go index f7aefa4fc..805809c15 100644 --- a/agent/backend/pktvisor/pktvisor.go +++ b/agent/backend/pktvisor/pktvisor.go @@ -77,9 +77,9 @@ type AppMetrics struct { } // note this needs to be stateless because it is calledfor multiple go routines -func (p *pktvisorBackend) request(url string, payload interface{}, method string, body io.Reader, contentType string) error { +func (p *pktvisorBackend) request(url string, payload interface{}, method string, body io.Reader, contentType string, timeout int32) error { client := http.Client{ - Timeout: time.Second * 5, + Timeout: time.Second * time.Duration(timeout), } alive, err := p.checkAlive() @@ -146,7 +146,15 @@ func (p *pktvisorBackend) checkAlive() (bool, error) { return true, nil } -func (p *pktvisorBackend) ApplyPolicy(data policies.PolicyData) error { +func (p *pktvisorBackend) ApplyPolicy(data policies.PolicyData, updatePolicy bool) error { + + if updatePolicy { + // To update a policy it's necessary first remove it and then apply a new version + err := p.RemovePolicy(data) + if err != nil { + p.logger.Warn("policy failed to remove", zap.String("policy_id", data.ID), zap.String("policy_name", data.Name), zap.Error(err)) + } + } p.logger.Debug("pktvisor policy apply", zap.String("policy_id", data.ID), zap.Any("data", data.Data)) @@ -166,7 +174,7 @@ func (p *pktvisorBackend) ApplyPolicy(data policies.PolicyData) error { } var resp map[string]interface{} - err = p.request("policies", &resp, http.MethodPost, bytes.NewBuffer(pyaml), "application/x-yaml") + err = p.request("policies", &resp, http.MethodPost, bytes.NewBuffer(pyaml), "application/x-yaml", 5) if err != nil { p.logger.Warn("yaml policy application failure", zap.String("policy_id", data.ID), zap.ByteString("policy", pyaml)) return err @@ -178,7 +186,7 @@ func (p *pktvisorBackend) ApplyPolicy(data policies.PolicyData) error { func (p *pktvisorBackend) RemovePolicy(data policies.PolicyData) error { var resp interface{} - err := p.request(fmt.Sprintf("policies/%s", data.Name), &resp, http.MethodDelete, nil, "") + err := p.request(fmt.Sprintf("policies/%s", data.Name), &resp, http.MethodDelete, nil, "", 10) if err != nil { return err } @@ -187,7 +195,7 @@ func (p *pktvisorBackend) RemovePolicy(data policies.PolicyData) error { func (p *pktvisorBackend) Version() (string, error) { var appMetrics AppMetrics - err := p.request("metrics/app", &appMetrics, http.MethodGet, nil, "") + err := p.request("metrics/app", &appMetrics, http.MethodGet, nil, "", 5) if err != nil { return "", err } @@ -382,8 +390,11 @@ func (p *pktvisorBackend) Stop() error { } p.scraper.Stop() - p.exporter.Shutdown(context.Background()) - p.receiver.Shutdown(context.Background()) + if p.scrapeOtel { + p.exporter.Shutdown(context.Background()) + p.receiver.Shutdown(context.Background()) + } + p.logger.Info("pktvisor process stopped", zap.Int("pid", finalStatus.PID), zap.Int("exit_code", finalStatus.Exit)) return nil } @@ -418,7 +429,7 @@ func (p *pktvisorBackend) Configure(logger *zap.Logger, repo policies.PolicyRepo func (p *pktvisorBackend) scrapeMetrics(period uint) (map[string]interface{}, error) { var metrics map[string]interface{} - err := p.request(fmt.Sprintf("policies/__all/metrics/bucket/%d", period), &metrics, http.MethodGet, nil, "") + err := p.request(fmt.Sprintf("policies/__all/metrics/bucket/%d", period), &metrics, http.MethodGet, nil, "", 5) if err != nil { return nil, err } @@ -427,7 +438,7 @@ func (p *pktvisorBackend) scrapeMetrics(period uint) (map[string]interface{}, er func (p *pktvisorBackend) GetCapabilities() (map[string]interface{}, error) { var taps interface{} - err := p.request("taps", &taps, http.MethodGet, nil, "") + err := p.request("taps", &taps, http.MethodGet, nil, "", 5) if err != nil { return nil, err } @@ -464,3 +475,15 @@ func createReceiver(ctx context.Context, exporter component.MetricsExporter, log } return receiver, nil } + +func (p *pktvisorBackend) FullReset() error { + if err := p.Stop(); err != nil { + p.logger.Error("failed to stop backend on restart procedure", zap.Error(err)) + return err + } + if err := p.Start(); err != nil { + p.logger.Error("failed to start backend on restart procedure", zap.Error(err)) + return err + } + return nil +} diff --git a/agent/comms.go b/agent/comms.go index a3c9dd04f..0733199e5 100644 --- a/agent/comms.go +++ b/agent/comms.go @@ -51,13 +51,15 @@ func (a *orbAgent) nameAgentRPCTopics(channelId string) { } func (a *orbAgent) unsubscribeGroupChannels() { - for _, channel := range a.groupChannels { - base := fmt.Sprintf("channels/%s/messages", channel) + for id, groupInfo := range a.groupsInfos { + base := fmt.Sprintf("channels/%s/messages", groupInfo.ChannelID) rpcFromCoreTopic := fmt.Sprintf("%s/%s", base, fleet.RPCFromCoreTopic) if token := a.client.Unsubscribe(rpcFromCoreTopic); token.Wait() && token.Error() != nil { - a.logger.Warn("failed to unsubscribe to group channel", zap.String("topic", channel), zap.Error(token.Error())) + a.logger.Warn("failed to unsubscribe to group channel", zap.String("group_id", id), zap.String("group_name", groupInfo.Name), zap.String("topic", groupInfo.ChannelID), zap.Error(token.Error())) } + a.logger.Info("completed RPC unsubscription to group", zap.String("group_id", id), zap.String("group_name", groupInfo.Name), zap.String("topic", rpcFromCoreTopic)) } + a.groupsInfos = make(map[string]GroupInfo) } func (a *orbAgent) unsubscribeGroupChannel(channelID string) { @@ -119,8 +121,7 @@ func (a *orbAgent) startComms(config config.MQTTConfig) error { return nil } -func (a *orbAgent) subscribeGroupChannels(groups []fleet.GroupMembershipData) []string { - var successList []string +func (a *orbAgent) subscribeGroupChannels(groups []fleet.GroupMembershipData) { for _, groupData := range groups { base := fmt.Sprintf("channels/%s/messages", groupData.ChannelID) @@ -128,20 +129,22 @@ func (a *orbAgent) subscribeGroupChannels(groups []fleet.GroupMembershipData) [] token := a.client.Subscribe(rpcFromCoreTopic, 1, a.handleGroupRPCFromCore) if token.Error() != nil { - a.logger.Error("failed to subscribe to group channel/topic", zap.String("topic", rpcFromCoreTopic), zap.Error(token.Error())) + a.logger.Error("failed to subscribe to group channel/topic", zap.String("group_id", groupData.GroupID), zap.String("group_name", groupData.Name), zap.String("topic", rpcFromCoreTopic), zap.Error(token.Error())) continue } ok := token.WaitTimeout(time.Second * 5) if ok && token.Error() != nil { - a.logger.Error("failed to subscribe to group channel/topic", zap.String("topic", rpcFromCoreTopic), zap.Error(token.Error())) + a.logger.Error("failed to subscribe to group channel/topic", zap.String("group_id", groupData.GroupID), zap.String("group_name", groupData.Name), zap.String("topic", rpcFromCoreTopic), zap.Error(token.Error())) continue } if !ok { - a.logger.Error("failed to subscribe to group channel/topic: time out", zap.String("topic", rpcFromCoreTopic)) + a.logger.Error("failed to subscribe to group channel/topic: time out", zap.String("group_id", groupData.GroupID), zap.String("group_name", groupData.Name), zap.String("topic", rpcFromCoreTopic)) continue } - a.logger.Info("completed RPC subscription to group", zap.String("name", groupData.Name), zap.String("topic", rpcFromCoreTopic)) - successList = append(successList, groupData.ChannelID) + a.logger.Info("completed RPC subscription to group", zap.String("group_id", groupData.GroupID), zap.String("group_name", groupData.Name), zap.String("topic", rpcFromCoreTopic)) + a.groupsInfos[groupData.GroupID] = GroupInfo{ + Name: groupData.Name, + ChannelID: groupData.ChannelID, + } } - return successList } diff --git a/agent/heartbeats.go b/agent/heartbeats.go index 8ba0cbcd9..fbf82aa4a 100644 --- a/agent/heartbeats.go +++ b/agent/heartbeats.go @@ -34,6 +34,7 @@ func (a *orbAgent) sendSingleHeartbeat(t time.Time, state fleet.State) { if err == nil { for _, pd := range pdata { ps[pd.ID] = fleet.PolicyStateInfo{ + Name: pd.Name, State: pd.State.String(), Error: pd.BackendErr, Datasets: pd.GetDatasetIDs(), @@ -43,12 +44,21 @@ func (a *orbAgent) sendSingleHeartbeat(t time.Time, state fleet.State) { a.logger.Error("unable to retrieved policy state", zap.Error(err)) } + ag := make(map[string]fleet.GroupStateInfo) + for id, groupInfo := range a.groupsInfos { + ag[id] = fleet.GroupStateInfo{ + GroupName: groupInfo.Name, + GroupChannel: groupInfo.ChannelID, + } + } + hbData := fleet.Heartbeat{ SchemaVersion: fleet.CurrentHeartbeatSchemaVersion, State: state, TimeStamp: t, BackendState: bes, PolicyState: ps, + GroupState: ag, } body, err := json.Marshal(hbData) diff --git a/agent/policyMgr/manager.go b/agent/policyMgr/manager.go index 2fd7ec974..0dce6b750 100644 --- a/agent/policyMgr/manager.go +++ b/agent/policyMgr/manager.go @@ -5,6 +5,7 @@ package manager import ( + "fmt" "github.com/jmoiron/sqlx" "github.com/ns1labs/orb/agent/backend" "github.com/ns1labs/orb/agent/config" @@ -18,6 +19,8 @@ type PolicyManager interface { RemovePolicyDataset(policyID string, datasetID string, be backend.Backend) GetPolicyState() ([]policies.PolicyData, error) GetRepo() policies.PolicyRepo + ApplyBackendPolicies(be backend.Backend) error + RemoveBackendPolicies(be backend.Backend) error } var _ PolicyManager = (*policyManager)(nil) @@ -65,6 +68,7 @@ func (a *policyManager) ManagePolicy(payload fleet.AgentPolicyRPCPayload) { Data: payload.Data, State: policies.Unknown, } + var updatePolicy bool if a.repo.Exists(payload.ID) { // we have already processed this policy id before (it may be running or failed) // ensure we are associating this dataset with this policy, if one was specified @@ -75,6 +79,19 @@ func (a *policyManager) ManagePolicy(payload fleet.AgentPolicyRPCPayload) { a.logger.Warn("policy failed to ensure dataset id", zap.String("policy_id", payload.ID), zap.String("policy_name", payload.Name), zap.String("dataset_id", payload.DatasetID), zap.Error(err)) } } + // if policy already exist and has no version upgrade, has no need to apply it again + currentPolicy, err := a.repo.Get(payload.ID) + if err != nil { + a.logger.Error("failed to retrieve policy", zap.String("policy_id", payload.ID), zap.Error(err)) + return + } + if currentPolicy.Version >= pd.Version { + a.logger.Info("a better version of this policy has already been applied, skipping", zap.String("policy_id", pd.ID), zap.String("policy_name", pd.Name), zap.String("attempted_version", fmt.Sprint(pd.Version)), zap.String("current_version", fmt.Sprint(currentPolicy.Version))) + return + } else { + updatePolicy = true + } + pd.Datasets = currentPolicy.Datasets } else { // new policy we have not seen before, associate with this dataset // on first time we see policy, we *require* dataset @@ -91,7 +108,7 @@ func (a *policyManager) ManagePolicy(payload fleet.AgentPolicyRPCPayload) { } else { // attempt to apply the policy to the backend. status of policy application (running/failed) is maintained there. be := backend.GetBackend(payload.Backend) - a.applyPolicy(payload, be, &pd) + a.applyPolicy(payload, be, &pd, updatePolicy) } // save policy (with latest status) to local policy db a.repo.Update(pd) @@ -106,7 +123,7 @@ func (a *policyManager) ManagePolicy(payload fleet.AgentPolicyRPCPayload) { return } be := backend.GetBackend(payload.Backend) - // Remove policy from pktvisor via http request + // Remove policy via http request err := be.RemovePolicy(pd) if err != nil { a.logger.Warn("policy failed to remove", zap.String("policy_id", payload.ID), zap.String("policy_name", payload.Name), zap.Error(err)) @@ -114,7 +131,7 @@ func (a *policyManager) ManagePolicy(payload fleet.AgentPolicyRPCPayload) { // Remove policy from orb-agent local repo err = a.repo.Remove(pd.ID) if err != nil { - a.logger.Warn("policy failed to remove local", zap.String("policy_id", pd.ID), zap.String("policy_name", pd.Name), zap.Error(err)) + a.logger.Warn("policy failed to remove local", zap.String("policy_id", pd.ID), zap.String("policy_name", pd.Name), zap.Error(err)) } break default: @@ -135,7 +152,7 @@ func (a *policyManager) RemovePolicyDataset(policyID string, datasetID string, b return } if removePolicy { - // Remove policy from pktvisor via http request + // Remove policy via http request err := be.RemovePolicy(policyData) if err != nil { a.logger.Warn("policy failed to remove", zap.String("policy_id", policyID), zap.String("policy_name", policyData.Name), zap.Error(err)) @@ -148,8 +165,8 @@ func (a *policyManager) RemovePolicyDataset(policyID string, datasetID string, b } } -func (a *policyManager) applyPolicy(payload fleet.AgentPolicyRPCPayload, be backend.Backend, pd *policies.PolicyData) { - err := be.ApplyPolicy(*pd) +func (a *policyManager) applyPolicy(payload fleet.AgentPolicyRPCPayload, be backend.Backend, pd *policies.PolicyData, updatePolicy bool) { + err := be.ApplyPolicy(*pd, updatePolicy) if err != nil { a.logger.Warn("policy failed to apply", zap.String("policy_id", payload.ID), zap.String("policy_name", payload.Name), zap.Error(err)) pd.State = policies.FailedToApply @@ -160,3 +177,45 @@ func (a *policyManager) applyPolicy(payload fleet.AgentPolicyRPCPayload, be back pd.BackendErr = "" } } + +func (a *policyManager) RemoveBackendPolicies(be backend.Backend) error { + plcies, err := a.repo.GetAll() + if err != nil { + a.logger.Error("failed to retrieve list of policies", zap.Error(err)) + return err + } + + for _, plcy := range plcies { + err := be.RemovePolicy(plcy) + if err != nil { + a.logger.Error("failed to remove policy from backend", zap.String("policy_id", plcy.ID), zap.String("policy_name", plcy.Name), zap.Error(err)) + return err + } + plcy.State = policies.Unknown + a.repo.Update(plcy) + } + return nil +} + +func (a *policyManager) ApplyBackendPolicies(be backend.Backend) error { + plcies, err := a.repo.GetAll() + if err != nil { + a.logger.Error("failed to retrieve list of policies", zap.Error(err)) + return err + } + + for _, policy := range plcies { + be.ApplyPolicy(policy, false) + if err != nil { + a.logger.Warn("policy failed to apply", zap.String("policy_id", policy.ID), zap.String("policy_name", policy.Name), zap.Error(err)) + policy.State = policies.FailedToApply + policy.BackendErr = err.Error() + } else { + a.logger.Info("policy applied successfully", zap.String("policy_id", policy.ID), zap.String("policy_name", policy.Name)) + policy.State = policies.Running + policy.BackendErr = "" + } + a.repo.Update(policy) + } + return nil +} diff --git a/agent/rpc_from.go b/agent/rpc_from.go index f4f16280d..638c23fdc 100644 --- a/agent/rpc_from.go +++ b/agent/rpc_from.go @@ -17,11 +17,10 @@ func (a *orbAgent) handleGroupMembership(rpc fleet.GroupMembershipRPCPayload) { // if this is the full list, reset all group subscriptions and subscribed to this list if rpc.FullList { a.unsubscribeGroupChannels() - a.groupChannels = a.subscribeGroupChannels(rpc.Groups) + a.subscribeGroupChannels(rpc.Groups) } else { // otherwise, just add these subscriptions to the existing list - successList := a.subscribeGroupChannels(rpc.Groups) - a.groupChannels = append(a.groupChannels, successList...) + a.subscribeGroupChannels(rpc.Groups) } } @@ -98,6 +97,10 @@ func (a *orbAgent) handleDatasetRemoval(rpc fleet.DatasetRemovedRPCPayload) { a.removeDatasetFromPolicy(rpc.DatasetID, rpc.PolicyID) } +func (a *orbAgent) handleAgentReset(payload fleet.AgentResetRPCPayload) { + a.Restart(payload.FullReset, payload.Reason) +} + func (a *orbAgent) handleRPCFromCore(client mqtt.Client, message mqtt.Message) { a.logger.Debug("RPC message from core", zap.String("topic", message.Topic()), zap.ByteString("payload", message.Payload())) @@ -139,6 +142,13 @@ func (a *orbAgent) handleRPCFromCore(client mqtt.Client, message mqtt.Message) { return } a.handleAgentStop(r.Payload) + case fleet.AgentResetRPCFunc: + var r fleet.AgentResetRPC + if err := json.Unmarshal(message.Payload(), &r); err != nil { + a.logger.Error("error decoding agent reset message from core", zap.Error(fleet.ErrSchemaMalformed)) + return + } + a.handleAgentReset(r.Payload) default: a.logger.Warn("unsupported/unhandled core RPC, ignoring", zap.String("func", rpc.Func), diff --git a/fleet/agent_group_service.go b/fleet/agent_group_service.go index 674985d97..16722403d 100644 --- a/fleet/agent_group_service.go +++ b/fleet/agent_group_service.go @@ -36,6 +36,10 @@ func (svc fleetService) removeAgentGroupSubscriptions(groupID string, ownerID st func (svc fleetService) addAgentsToAgentGroupChannel(token string, g AgentGroup) error { // first we get all agents, online or not, to connect them to the correct group channel list, err := svc.agentRepo.RetrieveAllByAgentGroupID(context.Background(), g.MFOwnerID, g.ID, false) + if err != nil{ + return err + } + if len(list) == 0 { return nil } @@ -102,15 +106,25 @@ func (svc fleetService) EditAgentGroup(ctx context.Context, token string, group return AgentGroup{}, errors.ErrUpdateEntity } + // Should return a list of agents before applying an edit + listUnsub, err := svc.agentRepo.RetrieveAllByAgentGroupID(context.Background(), ownerID, group.ID, true) + if err != nil { + return AgentGroup{}, err + } + ag, err := svc.agentGroupRepository.Update(ctx, ownerID, group) if err != nil { return AgentGroup{}, err } - list, err := svc.agentRepo.RetrieveAllByAgentGroupID(context.Background(), ownerID, group.ID, true) + listSub, err := svc.agentRepo.RetrieveAllByAgentGroupID(context.Background(), ownerID, group.ID, true) if err != nil { return AgentGroup{}, err } + + // append both lists and remove duplicates + // need to unsubscribe the agents who are no longer matching with the group + list := removeDuplicates(listSub, listUnsub) for _, agent := range list { err := svc.agentComms.NotifyAgentGroupMemberships(agent) if err != nil { @@ -121,6 +135,20 @@ func (svc fleetService) EditAgentGroup(ctx context.Context, token string, group return ag, nil } +func removeDuplicates(sliceA []Agent, sliceB []Agent) []Agent { + keys := make(map[string]bool) + var list []Agent + var concatSlice []Agent + concatSlice = append(append(concatSlice, sliceA...), sliceB...) + for _, entry := range concatSlice { + if _, value := keys[entry.MFThingID]; !value { + keys[entry.MFThingID] = true + list = append(list, entry) + } + } + return list +} + func (svc fleetService) ViewAgentGroupByIDInternal(ctx context.Context, groupID string, ownerID string) (AgentGroup, error) { return svc.agentGroupRepository.RetrieveByID(ctx, groupID, ownerID) } diff --git a/fleet/agent_group_service_test.go b/fleet/agent_group_service_test.go index 62ba0b34b..06086e795 100644 --- a/fleet/agent_group_service_test.go +++ b/fleet/agent_group_service_test.go @@ -17,6 +17,7 @@ import ( "github.com/mainflux/mainflux/things" thingsapi "github.com/mainflux/mainflux/things/api/things/http" "github.com/ns1labs/orb/fleet" + "github.com/ns1labs/orb/fleet/backend/pktvisor" flmocks "github.com/ns1labs/orb/fleet/mocks" "github.com/ns1labs/orb/pkg/errors" "github.com/ns1labs/orb/pkg/types" @@ -80,13 +81,14 @@ func newThingsServer(svc things.Service) *httptest.Server { func newService(auth mainflux.AuthServiceClient, url string) fleet.Service { agentGroupRepo := flmocks.NewAgentGroupRepository() agentRepo := flmocks.NewAgentRepositoryMock() - agentComms := flmocks.NewFleetCommService() + agentComms := flmocks.NewFleetCommService(agentRepo, agentGroupRepo) logger, _ := zap.NewDevelopment() config := mfsdk.Config{ BaseURL: url, } mfsdk := mfsdk.NewSDK(config) + pktvisor.Register(auth, agentRepo) return fleet.NewFleetService(logger, auth, agentRepo, agentGroupRepo, agentComms, mfsdk) } @@ -101,6 +103,20 @@ func TestCreateAgentGroup(t *testing.T) { nameID, err := types.NewIdentifier("eu-agents") require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + agNameID, err := types.NewIdentifier("agent") + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + agent := fleet.Agent{ + Name: agNameID, + MFOwnerID: ownerID.String(), + MFChannelID: "", + AgentTags: map[string]string{ + "region": "eu", + "node_type": "dns", + }, + } + _, err = fleetService.CreateAgent(context.Background(), token, agent) + validAgent := fleet.AgentGroup{ MFOwnerID: ownerID.String(), Name: nameID, @@ -297,6 +313,16 @@ func TestUpdateAgentGroup(t *testing.T) { ag, err := createAgentGroup(t, "ue-agent-group", fleetService) require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + agNameID, err := types.NewIdentifier("agent") + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + agent := fleet.Agent{ + Name: agNameID, + MFOwnerID: ag.MFOwnerID, + MFChannelID: ag.MFChannelID, + } + _, err = fleetService.CreateAgent(context.Background(), token, agent) + matching := types.Metadata{"total": 0, "online": 0} wrongAgentGroup := fleet.AgentGroup{ID: wrongID} readyOnlyAgentGroup := fleet.AgentGroup{ID: wrongID, MatchingAgents: matching} diff --git a/fleet/agent_service.go b/fleet/agent_service.go index 0500da488..5593df26b 100644 --- a/fleet/agent_service.go +++ b/fleet/agent_service.go @@ -62,6 +62,20 @@ func (svc fleetService) ViewAgentByID(ctx context.Context, token string, thingID return svc.agentRepo.RetrieveByID(ctx, ownerID, thingID) } +func (svc fleetService) ResetAgent(ctx context.Context, token string, agentID string) error { + ownerID, err := svc.identify(token) + if err != nil { + return err + } + + agent, err := svc.agentRepo.RetrieveByID(ctx, ownerID, agentID) + if err != nil { + return err + } + + return svc.agentComms.NotifyAgentReset(agent.MFChannelID, true, "Reset initiated from control plane") +} + func (svc fleetService) ViewAgentByIDInternal(ctx context.Context, ownerID string, id string) (Agent, error) { return svc.agentRepo.RetrieveByID(ctx, ownerID, id) } @@ -161,6 +175,12 @@ func (svc fleetService) EditAgent(ctx context.Context, token string, agent Agent return Agent{}, err } + err = svc.addAgentToAgentGroupChannels(token, res) + if err != nil { + // TODO should we roll back? + svc.logger.Error("failed to add agent to a existing group channel", zap.String("agent_id", res.MFThingID), zap.Error(err)) + } + err = svc.agentComms.NotifyAgentGroupMemberships(res) if err != nil { svc.logger.Error("failure during agent group membership comms", zap.Error(err)) diff --git a/fleet/agent_service_test.go b/fleet/agent_service_test.go index a76b83551..96c3b00f7 100644 --- a/fleet/agent_service_test.go +++ b/fleet/agent_service_test.go @@ -193,9 +193,23 @@ func TestUpdateAgent(t *testing.T) { thingsServer := newThingsServer(newThingsService(users)) fleetService := newService(users, thingsServer.URL) - ag, err := createAgent(t, "my-agent1", fleetService) + validAgentName, err := types.NewIdentifier("group") + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + ag, err := fleetService.CreateAgent(context.Background(), "token", fleet.Agent{ + Name: validAgentName, + AgentTags: map[string]string{"test": "true"}, + }) require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + validName, err := types.NewIdentifier("group") + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + _, _ = fleetService.CreateAgentGroup(context.Background(), "token", fleet.AgentGroup{ + Name: validName, + Tags: map[string]string{"test": "true"}, + }) + wrongAgentGroup := fleet.Agent{MFThingID: wrongID} cases := map[string]struct { group fleet.Agent @@ -281,6 +295,8 @@ func TestCreateAgent(t *testing.T) { nameID, err := types.NewIdentifier("eu-agents") require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + conflictCase, err := createAgent(t, "agent", fleetService) + validAgent := fleet.Agent{ MFOwnerID: ownerID.String(), Name: nameID, @@ -306,6 +322,11 @@ func TestCreateAgent(t *testing.T) { token: invalidToken, err: fleet.ErrUnauthorizedAccess, }, + "add a conflict agent": { + agent: conflictCase, + token: token, + err: fleet.ErrConflict, + }, } for desc, tc := range cases { @@ -383,6 +404,79 @@ func TestListBackends(t *testing.T) { } } +func TestViewAgentBackend(t *testing.T) { + users := flmocks.NewAuthService(map[string]string{token: email}) + + thingsServer := newThingsServer(newThingsService(users)) + fleetService := newService(users, thingsServer.URL) + + cases := map[string]struct { + name string + token string + err error + }{ + "view backend not registered": { + name: "invalid", + token: token, + err: errors.ErrNotFound, + }, + "view backend with invalid token": { + name: "pktvisor", + token: invalidToken, + err: errors.ErrUnauthorizedAccess, + }, + "view registered backend": { + name: "pktvisor", + token: token, + err: nil, + }, + } + + for desc, tc := range cases { + t.Run(desc, func(t *testing.T) { + _, err := fleetService.ViewAgentBackend(context.Background(), tc.token, tc.name) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", desc, tc.err, err)) + }) + } +} + +func TestViewOwnerInternal(t *testing.T) { + users := flmocks.NewAuthService(map[string]string{token: email}) + + thingsServer := newThingsServer(newThingsService(users)) + fleetService := newService(users, thingsServer.URL) + + ag, err := createAgent(t, "agent", fleetService) + + chID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + cases := map[string]struct { + channelID string + agent fleet.Agent + err error + }{ + "view existent owner by channelID": { + channelID: ag.MFChannelID, + agent: ag, + err: nil, + }, + "view existent owner by non-existent channelID": { + channelID: chID.String(), + agent: fleet.Agent{}, + err: errors.ErrNotFound, + }, + } + + for desc, tc := range cases { + t.Run(desc, func(t *testing.T) { + agent, err := fleetService.ViewOwnerByChannelIDInternal(context.Background(), tc.channelID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", desc, tc.err, err)) + assert.Equal(t, tc.agent, agent, fmt.Sprintf("%s: expected %s got %s", desc, tc.agent, agent)) + }) + } +} + func createAgent(t *testing.T, name string, svc fleet.Service) (fleet.Agent, error) { t.Helper() aCopy := agent diff --git a/fleet/agents.go b/fleet/agents.go index c9b5cb6f2..0af77bc74 100644 --- a/fleet/agents.go +++ b/fleet/agents.go @@ -92,6 +92,8 @@ type AgentService interface { ViewAgentBackend(ctx context.Context, token string, name string) (interface{}, error) //ViewOwnerByChannelIDInternal return a correspondent ownerID by a provided channel id ViewOwnerByChannelIDInternal(ctx context.Context, channelID string) (Agent, error) + // ResetAgent reset a agent on edge by a provided agent + ResetAgent(ct context.Context, token string, agentID string) error } type AgentRepository interface { diff --git a/fleet/api/http/endpoint.go b/fleet/api/http/endpoint.go index 870e4ee69..bdd410265 100644 --- a/fleet/api/http/endpoint.go +++ b/fleet/api/http/endpoint.go @@ -213,7 +213,7 @@ func viewAgentEndpoint(svc fleet.Service) endpoint.Endpoint { return nil, err } - res := viewAgentRes{ + res := agentRes{ ID: ag.MFThingID, Name: ag.Name.String(), ChannelID: ag.MFChannelID, @@ -223,12 +223,25 @@ func viewAgentEndpoint(svc fleet.Service) endpoint.Endpoint { AgentMetadata: ag.AgentMetadata, State: ag.State.String(), LastHBData: ag.LastHBData, - LastHB: ag.LastHB, + TsLastHB: ag.LastHB, } return res, nil } } +func resetAgentEndpoint(svc fleet.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (response interface{}, err error) { + req := request.(viewResourceReq) + if err := req.validate(); err != nil { + return nil, err + } + if err := svc.ResetAgent(ctx, req.token, req.id); err != nil { + return nil, err + } + return response, nil + } +} + func listAgentsEndpoint(svc fleet.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(listResourcesReq) @@ -250,10 +263,10 @@ func listAgentsEndpoint(svc fleet.Service) endpoint.Endpoint { Order: page.Order, Dir: page.Dir, }, - Agents: []viewAgentRes{}, + Agents: []agentRes{}, } for _, ag := range page.Agents { - view := viewAgentRes{ + view := agentRes{ ID: ag.MFThingID, Name: ag.Name.String(), ChannelID: ag.MFChannelID, @@ -261,7 +274,7 @@ func listAgentsEndpoint(svc fleet.Service) endpoint.Endpoint { OrbTags: ag.OrbTags, TsCreated: ag.Created, State: ag.State.String(), - LastHB: ag.LastHB, + TsLastHB: ag.LastHB, } res.Agents = append(res.Agents, view) } @@ -324,7 +337,7 @@ func editAgentEndpoint(svc fleet.Service) endpoint.Endpoint { return nil, err } - res := viewAgentRes{ + res := agentRes{ ID: ag.MFThingID, Name: ag.Name.String(), ChannelID: ag.MFChannelID, @@ -334,7 +347,7 @@ func editAgentEndpoint(svc fleet.Service) endpoint.Endpoint { AgentMetadata: ag.AgentMetadata, State: ag.State.String(), LastHBData: ag.LastHBData, - LastHB: ag.LastHB, + TsLastHB: ag.LastHB, } return res, nil diff --git a/fleet/api/http/endpoint_test.go b/fleet/api/http/endpoint_test.go index 9262b169d..937785850 100644 --- a/fleet/api/http/endpoint_test.go +++ b/fleet/api/http/endpoint_test.go @@ -130,7 +130,7 @@ func newThingsServer(svc things.Service) *httptest.Server { func newService(auth mainflux.AuthServiceClient, url string) fleet.Service { agentGroupRepo := flmocks.NewAgentGroupRepository() agentRepo := flmocks.NewAgentRepositoryMock() - agentComms := flmocks.NewFleetCommService() + agentComms := flmocks.NewFleetCommService(agentRepo, agentGroupRepo) logger, _ := zap.NewDevelopment() config := mfsdk.Config{ BaseURL: url, @@ -155,6 +155,9 @@ func TestCreateAgentGroup(t *testing.T) { cli := newClientServer(t) defer cli.server.Close() + var missingTagsJson = "{\n \"name\": \"group\", \n \"tags\": {}, \n \"description\": \"An example agent group representing european dns nodes\", \n \"validate_only\": false \n}" + var invalidNameJson = "{\n \"name\": \"g\", \n \"tags\": {\n \"region\": \"eu\", \n \"node_type\": \"dns\"\n }, \n \"description\": \"An example agent group representing european dns nodes\", \n \"validate_only\": false \n}" + // Conflict scenario createAgentGroup(t, "eu-agents-conflict", &cli) @@ -200,6 +203,20 @@ func TestCreateAgentGroup(t *testing.T) { status: http.StatusUnsupportedMediaType, location: "/agent_groups", }, + "add a agent group without tags": { + req: missingTagsJson, + contentType: contentType, + auth: token, + status: http.StatusBadRequest, + location: "/agent_groups", + }, + "add a agent group with invalid name": { + req: invalidNameJson, + contentType: contentType, + auth: token, + status: http.StatusBadRequest, + location: "/agent_groups", + }, } for desc, tc := range cases { @@ -455,6 +472,18 @@ func TestUpdateAgentGroup(t *testing.T) { Tags: ag.Tags, }) + invalidName := toJSON(updateAgentGroupReq{ + Name: "g", + Description: ag.Description, + Tags: ag.Tags, + }) + + missingTagsdata := toJSON(updateAgentGroupReq{ + Name: ag.Name.String(), + Description: ag.Description, + Tags: map[string]string{}, + }) + cases := map[string]struct { req string id string @@ -539,6 +568,20 @@ func TestUpdateAgentGroup(t *testing.T) { auth: token, status: http.StatusBadRequest, }, + "add a agent group with invalid name": { + req: invalidName, + id: ag.ID, + contentType: contentType, + auth: token, + status: http.StatusBadRequest, + }, + "update existing agent group without tags": { + req: missingTagsdata, + id: ag.ID, + contentType: contentType, + auth: token, + status: http.StatusBadRequest, + }, } for desc, tc := range cases { @@ -611,10 +654,12 @@ func TestValidateAgentGroup(t *testing.T) { cli := newClientServer(t) defer cli.server.Close() - var invalidValueTag = "{\n \"name\": \"eu-agents\", \n \"tags\": {\n \"invalidTag\", \n \"node_type\": \"dns\"\n }, \n \"description\": \"An example agent group representing european dns nodes\", \n \"validate_only\": false \n}" - var invalidValueName = "{\n \"name\": \",,AGENT 6,\", \n \"tags\": {\n \"region\": \"eu\", \n \"node_type\": \"dns\"\n }, \n \"description\": \"An example agent group representing european dns nodes\", \n \"validate_only\": false \n}" - var invalidField = "{\n \"nname\": \",,AGENT 6,\", \n \"tags\": {\n \"region\": \"eu\", \n \"node_type\": \"dns\"\n }, \n \"description\": \"An example agent group representing european dns nodes\", \n \"validate_only\": false \n}" - + var ( + invalidValueTag = "{\n \"name\": \"eu-agents\", \n \"tags\": {\n \"invalidTag\", \n \"node_type\": \"dns\"\n }, \n \"description\": \"An example agent group representing european dns nodes\", \n \"validate_only\": false \n}" + invalidValueName = "{\n \"name\": \",,AGENT 6,\", \n \"tags\": {\n \"region\": \"eu\", \n \"node_type\": \"dns\"\n }, \n \"description\": \"An example agent group representing european dns nodes\", \n \"validate_only\": false \n}" + invalidField = "{\n \"nname\": \",,AGENT 6,\", \n \"tags\": {\n \"region\": \"eu\", \n \"node_type\": \"dns\"\n }, \n \"description\": \"An example agent group representing european dns nodes\", \n \"validate_only\": false \n}" + invalidNameJson = "{\n \"name\": \"g\", \n \"tags\": {\n \"region\": \"eu\", \n \"node_type\": \"dns\"\n }, \n \"description\": \"An example agent group representing european dns nodes\", \n \"validate_only\": false \n}" + ) cases := map[string]struct { req string contentType string @@ -659,7 +704,7 @@ func TestValidateAgentGroup(t *testing.T) { status: http.StatusBadRequest, location: "/agent_groups/validate", }, - "validate a agent group with a invalid name": { + "validate a agent group with a name not respecting RegEx": { req: invalidValueName, contentType: contentType, auth: token, @@ -680,6 +725,13 @@ func TestValidateAgentGroup(t *testing.T) { status: http.StatusBadRequest, location: "/agent_groups/validate", }, + "validate a agent group with a invalid name": { + req: invalidNameJson, + contentType: contentType, + auth: token, + status: http.StatusBadRequest, + location: "/agent_groups/validate", + }, } for desc, tc := range cases { @@ -731,6 +783,11 @@ func TestViewAgent(t *testing.T) { auth: "", status: http.StatusUnauthorized, }, + "view a existing agent with empty id": { + id: "", + auth: token, + status: http.StatusBadRequest, + }, } for desc, tc := range cases { @@ -751,11 +808,11 @@ func TestViewAgent(t *testing.T) { func TestListAgent(t *testing.T) { cli := newClientServer(t) - var data []viewAgentRes + var data []agentRes for i := 0; i < limit; i++ { ag, err := createAgent(t, fmt.Sprintf("my-agent-%d", i), &cli) require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - data = append(data, viewAgentRes{ + data = append(data, agentRes{ ID: ag.MFThingID, Name: ag.Name.String(), ChannelID: ag.MFChannelID, @@ -765,7 +822,7 @@ func TestListAgent(t *testing.T) { AgentMetadata: ag.AgentMetadata, State: ag.State.String(), LastHBData: ag.LastHBData, - LastHB: ag.LastHB, + TsLastHB: ag.LastHB, }) } @@ -773,7 +830,7 @@ func TestListAgent(t *testing.T) { auth string status int url string - res []viewAgentRes + res []agentRes total uint64 }{ "retrieve a list of agents": { @@ -936,6 +993,11 @@ func TestUpdateAgent(t *testing.T) { Tags: ag.OrbTags, }) + invalidNameData := toJSON(updateAgentReq{ + Name: "a", + Tags: ag.OrbTags, + }) + cases := map[string]struct { req string id string @@ -1020,6 +1082,13 @@ func TestUpdateAgent(t *testing.T) { auth: token, status: http.StatusBadRequest, }, + "update existing agent with invalid name": { + req: invalidNameData, + id: ag.MFThingID, + contentType: contentType, + auth: token, + status: http.StatusBadRequest, + }, } for desc, tc := range cases { @@ -1040,10 +1109,13 @@ func TestUpdateAgent(t *testing.T) { } func TestValidateAgent(t *testing.T) { - var validJson = "{\"name\":\"eu-agents\",\"orb_tags\": {\"region\":\"eu\", \"node_type\":\"dns\"}}" - var invalidTag = "{\"name\":\"eu-agents\",\"orb_tags\": {\n\"invalidTag\", \n \"node_type\":\"dns\"}}" - var invalidName = "{\"name\":\",,AGENT 6,\",\"orb_tags\": {\"region\":\"eu\", \"node_type\":\"dns\"}}" - var invalidField = "{\"nname\":\"eu-agents\",\"orb_tags\": {\"region\":\"eu\", \"node_type\":\"dns\"}}" + var ( + validJson = "{\"name\":\"eu-agents\",\"orb_tags\": {\"region\":\"eu\", \"node_type\":\"dns\"}}" + invalidTag = "{\"name\":\"eu-agents\",\"orb_tags\": {\n\"invalidTag\", \n \"node_type\":\"dns\"}}" + invalidName = "{\"name\":\",,AGENT 6,\",\"orb_tags\": {\"region\":\"eu\", \"node_type\":\"dns\"}}" + invalidField = "{\"nname\":\"eu-agents\",\"orb_tags\": {\"region\":\"eu\", \"node_type\":\"dns\"}}" + invalidNameJson = "{\"name\":\"a\",\"orb_tags\": {\"region\":\"eu\", \"node_type\":\"dns\"}}" + ) cli := newClientServer(t) defer cli.server.Close() @@ -1090,7 +1162,7 @@ func TestValidateAgent(t *testing.T) { status: http.StatusBadRequest, location: "/agents/validate", }, - "validate a agent with a invalid name": { + "validate a agent with name not following regex": { req: invalidName, contentType: contentType, auth: token, @@ -1111,6 +1183,13 @@ func TestValidateAgent(t *testing.T) { status: http.StatusBadRequest, location: "/agents/validate", }, + "validate a agent with invalid name": { + req: invalidNameJson, + contentType: contentType, + auth: token, + status: http.StatusBadRequest, + location: "/agents/validate", + }, } for desc, tc := range cases { @@ -1134,6 +1213,7 @@ func TestValidateAgent(t *testing.T) { func TestCreateAgent(t *testing.T) { var validJson = "{\"name\":\"eu-agents\",\"orb_tags\": {\"region\":\"eu\", \"node_type\":\"dns\"}}" var conflictValidJson = "{\"name\":\"conflict\",\"orb_tags\": {\"region\":\"eu\", \"node_type\":\"dns\"}}" + var invalidNameJson = "{\"name\":\"a\",\"orb_tags\": {\"region\":\"eu\", \"node_type\":\"dns\"}}" cli := newClientServer(t) defer cli.server.Close() @@ -1197,6 +1277,13 @@ func TestCreateAgent(t *testing.T) { status: http.StatusBadRequest, location: "/agents", }, + "add a agent with invalid name": { + req: invalidNameJson, + contentType: contentType, + auth: token, + status: http.StatusBadRequest, + location: "/agents", + }, } for desc, tc := range cases { @@ -1281,6 +1368,10 @@ func TestAgentBackends(t *testing.T) { auth: invalidToken, status: http.StatusUnauthorized, }, + "Return a list of available backends with empty token": { + auth: "", + status: http.StatusUnauthorized, + }, } for desc, tc := range cases { @@ -1479,25 +1570,26 @@ type agentGroupsPageRes struct { Limit uint64 `json:"limit"` AgentGroups []agentGroupRes `json:"agentGroups"` } - -type viewAgentRes struct { +type agentRes struct { ID string `json:"id"` Name string `json:"name"` + State string `json:"state"` + Key string `json:"key,omitempty"` ChannelID string `json:"channel_id,omitempty"` AgentTags types.Tags `json:"agent_tags"` OrbTags types.Tags `json:"orb_tags"` - TsCreated time.Time `json:"ts_created"` AgentMetadata types.Metadata `json:"agent_metadata"` - State string `json:"state"` LastHBData types.Metadata `json:"last_hb_data"` - LastHB time.Time `json:"ts_last_hb"` + TsCreated time.Time `json:"ts_created"` + TsLastHB time.Time `json:"ts_last_hb"` + created bool } type agentsPageRes struct { - Total uint64 `json:"total"` - Offset uint64 `json:"offset"` - Limit uint64 `json:"limit"` - Agents []viewAgentRes `json:"agents"` + Total uint64 `json:"total"` + Offset uint64 `json:"offset"` + Limit uint64 `json:"limit"` + Agents []agentRes `json:"agents"` } type updateAgentGroupReq struct { diff --git a/fleet/api/http/logging.go b/fleet/api/http/logging.go index 5d505631d..c3db13b48 100644 --- a/fleet/api/http/logging.go +++ b/fleet/api/http/logging.go @@ -18,6 +18,20 @@ type loggingMiddleware struct { svc fleet.Service } +func (l loggingMiddleware) ResetAgent(ct context.Context, token string, agentID string) (err error) { + defer func(begin time.Time) { + if err != nil { + l.logger.Warn("method call: reset_agent", + zap.Error(err), + zap.Duration("duration", time.Since(begin))) + } else { + l.logger.Info("method call: reset_agent", + zap.Duration("duration", time.Since(begin))) + } + }(time.Now()) + return l.svc.ResetAgent(ct, token, agentID) +} + func (l loggingMiddleware) ViewOwnerByChannelIDInternal(ctx context.Context, channelID string) (_ fleet.Agent, err error) { defer func(begin time.Time) { if err != nil { diff --git a/fleet/api/http/metrics.go b/fleet/api/http/metrics.go index c63066254..9aa0386a8 100644 --- a/fleet/api/http/metrics.go +++ b/fleet/api/http/metrics.go @@ -22,6 +22,28 @@ type metricsMiddleware struct { auth mainflux.AuthServiceClient } +func (m metricsMiddleware) ResetAgent(ct context.Context, token string, agentID string) error { + ownerID, err := m.identify(token) + if err!= nil { + return err + } + + defer func(begin time.Time) { + labels := []string{ + "method", "resetAgent", + "owner_id", ownerID, + "agent_id", agentID, + "group_id", "", + } + + m.counter.With(labels...).Add(1) + m.latency.With(labels...).Observe(float64(time.Since(begin).Microseconds())) + + }(time.Now()) + + return m.svc.ResetAgent(ct, token, agentID) +} + func (m metricsMiddleware) ViewOwnerByChannelIDInternal(ctx context.Context, channelID string) (agent fleet.Agent, _ error) { defer func(begin time.Time) { labels := []string{ diff --git a/fleet/api/http/requests.go b/fleet/api/http/requests.go index c91f9b08f..bec0dab1b 100644 --- a/fleet/api/http/requests.go +++ b/fleet/api/http/requests.go @@ -116,9 +116,6 @@ func (req updateAgentReq) validate() error { if req.Name == "" { return errors.ErrMalformedEntity } - if len(req.Tags) == 0 { - return errors.ErrMalformedEntity - } _, err := types.NewIdentifier(req.Name) if err != nil { diff --git a/fleet/api/http/responses.go b/fleet/api/http/responses.go index 5778d4e2b..6417fb3f5 100644 --- a/fleet/api/http/responses.go +++ b/fleet/api/http/responses.go @@ -89,34 +89,9 @@ func (s agentRes) Empty() bool { return false } -type viewAgentRes struct { - ID string `json:"id"` - Name string `json:"name"` - ChannelID string `json:"channel_id,omitempty"` - AgentTags types.Tags `json:"agent_tags"` - OrbTags types.Tags `json:"orb_tags"` - TsCreated time.Time `json:"ts_created"` - AgentMetadata types.Metadata `json:"agent_metadata"` - State string `json:"state"` - LastHBData types.Metadata `json:"last_hb_data"` - LastHB time.Time `json:"ts_last_hb"` -} - -func (res viewAgentRes) Code() int { - return http.StatusOK -} - -func (res viewAgentRes) Headers() map[string]string { - return map[string]string{} -} - -func (res viewAgentRes) Empty() bool { - return false -} - type agentsPageRes struct { pageRes - Agents []viewAgentRes `json:"agents"` + Agents []agentRes `json:"agents"` } func (res agentsPageRes) Code() int { diff --git a/fleet/api/http/transport.go b/fleet/api/http/transport.go index d8c35791d..1e66fcf30 100644 --- a/fleet/api/http/transport.go +++ b/fleet/api/http/transport.go @@ -90,6 +90,11 @@ func MakeHandler(tracer opentracing.Tracer, svcName string, svc fleet.Service) h decodeListBackends, types.EncodeResponse, opts...)) + r.Post("/agents/:id/rpc/reset", kithttp.NewServer( + kitot.TraceServer(tracer, "reset_agent")(resetAgentEndpoint(svc)), + decodeView, + types.EncodeResponse, + opts...)) r.Get("/agents/:id", kithttp.NewServer( kitot.TraceServer(tracer, "edit_agent")(viewAgentEndpoint(svc)), decodeView, diff --git a/fleet/comms.go b/fleet/comms.go index 618479989..1996899de 100644 --- a/fleet/comms.go +++ b/fleet/comms.go @@ -47,6 +47,8 @@ type AgentCommsService interface { NotifyGroupDatasetRemoval(ag AgentGroup, dsID string, policyID string) error // NotifyGroupPolicyUpdate RPC core -> Agent: Notify AgentGroup that a Policy has been updated NotifyGroupPolicyUpdate(ctx context.Context, ag AgentGroup, policyID string, ownerID string) error + //NotifyAgentReset RPC core -> Agent: Notify Agent to reset the backend + NotifyAgentReset(channelID string, fullReset bool, reason string) error } var _ AgentCommsService = (*fleetCommsService)(nil) @@ -115,7 +117,7 @@ func (svc fleetCommsService) NotifyGroupNewDataset(ctx context.Context, ag Agent func (svc fleetCommsService) NotifyAgentNewGroupMembership(a Agent, ag AgentGroup) error { payload := GroupMembershipRPCPayload{ - Groups: []GroupMembershipData{{Name: ag.Name.String(), ChannelID: ag.MFChannelID}}, + Groups: []GroupMembershipData{{GroupID: ag.ID, Name: ag.Name.String(), ChannelID: ag.MFChannelID}}, FullList: false, } @@ -230,6 +232,7 @@ func (svc fleetCommsService) NotifyAgentGroupMemberships(a Agent) error { fullList := make([]GroupMembershipData, len(list)) for i, agentGroup := range list { + fullList[i].GroupID = agentGroup.ID fullList[i].Name = agentGroup.Name.String() fullList[i].ChannelID = agentGroup.MFChannelID } @@ -434,6 +437,35 @@ func (svc fleetCommsService) NotifyAgentStop(MFChannelID string, reason string) return nil } +func (svc fleetCommsService) NotifyAgentReset(MFChannelID string, fullReset bool, reason string) error { + payload := AgentResetRPCPayload{ + FullReset: fullReset, + Reason: reason, + } + data := RPC{ + SchemaVersion: CurrentRPCSchemaVersion, + Func: AgentResetRPCFunc, + Payload: payload, + } + + body, err := json.Marshal(data) + if err != nil { + return err + } + + msg := messaging.Message{ + Channel: MFChannelID, + Subtopic: RPCFromCoreTopic, + Publisher: publisher, + Payload: body, + Created: time.Now().UnixNano(), + } + if err := svc.agentPubSub.Publish(msg.Channel, msg); err != nil { + return err + } + return nil +} + func NewFleetCommsService(logger *zap.Logger, policyClient pb.PolicyServiceClient, agentRepo AgentRepository, agentGroupRepo AgentGroupRepository, agentPubSub mfnats.PubSub) AgentCommsService { return &fleetCommsService{ logger: logger, @@ -510,11 +542,14 @@ func (svc fleetCommsService) handleHeartbeat(thingID string, channelID string, p agent.State = Offline agent.LastHBData["backend_state"] = BackendStateInfo{} agent.LastHBData["policy_state"] = PolicyStateInfo{} + agent.LastHBData["group_state"] = GroupStateInfo{} } else { // otherwise, state is always "online" agent.State = Online agent.LastHBData["backend_state"] = hb.BackendState agent.LastHBData["policy_state"] = hb.PolicyState + agent.LastHBData["group_state"] = hb.GroupState + } err := svc.agentRepo.UpdateHeartbeatByIDWithChannel(context.Background(), agent) if err != nil { diff --git a/fleet/comms_rpc.go b/fleet/comms_rpc.go index a1a657e6c..0525dbaa7 100644 --- a/fleet/comms_rpc.go +++ b/fleet/comms_rpc.go @@ -23,6 +23,7 @@ type GroupMembershipRPC struct { } type GroupMembershipData struct { + GroupID string `json:"group_id"` Name string `json:"name"` ChannelID string `json:"channel_id"` } @@ -88,6 +89,19 @@ type AgentStopRPC struct { Payload AgentStopRPCPayload `json:"payload"` } +const AgentResetRPCFunc = "agent_reset" + +type AgentResetRPCPayload struct { + FullReset bool `json:"full_reset"` + Reason string `json:"reason"` +} + +type AgentResetRPC struct { + SchemaVersion string `json:"schema_version"` + Func string `json:"func"` + Payload AgentResetRPCPayload `json:"payload"` +} + // Edge -> Core const GroupMembershipReqRPCFunc = "group_membership_req" diff --git a/fleet/comms_test.go b/fleet/comms_test.go new file mode 100644 index 000000000..cf33882c8 --- /dev/null +++ b/fleet/comms_test.go @@ -0,0 +1,581 @@ +package fleet_test + +import ( + "context" + "fmt" + "github.com/gofrs/uuid" + "github.com/mainflux/mainflux" + mflog "github.com/mainflux/mainflux/logger" + mfsdk "github.com/mainflux/mainflux/pkg/sdk/go" + "github.com/ns1labs/orb/fleet" + "github.com/ns1labs/orb/fleet/backend/pktvisor" + flmocks "github.com/ns1labs/orb/fleet/mocks" + "github.com/ns1labs/orb/pkg/config" + "github.com/ns1labs/orb/pkg/errors" + "github.com/ns1labs/orb/pkg/types" + "github.com/ns1labs/orb/policies" + policyGRPC "github.com/ns1labs/orb/policies/api/grpc" + plmocks "github.com/ns1labs/orb/policies/mocks" + "github.com/ns1labs/orb/policies/pb" + sinkmocks "github.com/ns1labs/orb/sinks/mocks" + "github.com/opentracing/opentracing-go/mocktracer" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/grpc/test/bufconn" + "log" + "net" + "os" + "testing" +) + +const bufSize = 1024 * 1024 + +var ( + lis *bufconn.Listener + users = flmocks.NewAuthService(map[string]string{token: email}) + policiesSVC = newPoliciesService(users) +) + +func newFleetService(auth mainflux.AuthServiceClient, url string, agentGroupRepo fleet.AgentGroupRepository, agentRepo fleet.AgentRepository) fleet.Service { + agentComms := flmocks.NewFleetCommService(agentRepo, agentGroupRepo) + logger, err := zap.NewDevelopment() + if err != nil { + log.Fatalf("%v", err) + } + config := mfsdk.Config{ + BaseURL: url, + } + + mfsdk := mfsdk.NewSDK(config) + pktvisor.Register(auth, agentRepo) + return fleet.NewFleetService(logger, auth, agentRepo, agentGroupRepo, agentComms, mfsdk) +} + +func newPoliciesService(auth mainflux.AuthServiceClient) policies.Service { + policyRepo := plmocks.NewPoliciesRepository() + fleetGrpcClient := flmocks.NewClient() + SinkServiceClient := sinkmocks.NewClient() + + logger, err := zap.NewDevelopment() + if err != nil { + log.Fatalf("%v", err) + } + + return policies.New(logger, auth, policyRepo, fleetGrpcClient, SinkServiceClient) +} + +func init() { + lis = bufconn.Listen(bufSize) + server := grpc.NewServer() + + tracer := mocktracer.New() + policyServer := policyGRPC.NewServer(tracer, policiesSVC) + + pb.RegisterPolicyServiceServer(server, policyServer) + go func() { + if err := server.Serve(lis); err != nil { + log.Fatalf("Server exited with error: %v", err) + } + }() +} + +func bufDialer(context.Context, string) (net.Conn, error) { + return lis.Dial() +} + +func newCommsService(agentGroupRepo fleet.AgentGroupRepository, agentRepo fleet.AgentRepository) fleet.AgentCommsService { + logger, err := zap.NewDevelopment() + if err != nil { + log.Fatalf("error: %v", err) + } + + conn, err := grpc.DialContext(context.Background(), "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithInsecure()) + if err != nil { + log.Fatalf("Failed to dial bufnet: %v", err) + } + policyClient := pb.NewPolicyServiceClient(conn) + + mflogger, err := mflog.New(os.Stdout, "debug") + if err != nil { + log.Fatalf(err.Error()) + } + + url := config.LoadNatsConfig("orb_fleet") + agentPubSub, err := flmocks.NewPubSub(url.URL, "fleet", mflogger) + if err != nil { + log.Fatalf("Failed to create PubSub %v", err) + } + + return fleet.NewFleetCommsService(logger, policyClient, agentRepo, agentGroupRepo, agentPubSub) +} + +func TestNotifyGroupNewDataset(t *testing.T) { + agentGroupRepo := flmocks.NewAgentGroupRepository() + agentRepo := flmocks.NewAgentRepositoryMock() + + commsSVC := newCommsService(agentGroupRepo, agentRepo) + + thingsServer := newThingsServer(newThingsService(users)) + fleetSVC := newFleetService(users, thingsServer.URL, agentGroupRepo, agentRepo) + + ag, err := createAgentGroup(t, "group", fleetSVC) + assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + policy := createPolicy(t, policiesSVC, "policy") + dataset := createDataset(t, policiesSVC, "dataset", ag.ID) + + wrongPolicyID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + cases := map[string]struct { + policyID string + ownerID string + datasetID string + agentGroup fleet.AgentGroup + err error + }{ + "Notify a existent group new dataset": { + ownerID: ag.MFOwnerID, + policyID: policy.ID, + datasetID: dataset.ID, + agentGroup: ag, + err: nil, + }, + "Notify a existent group new dataset with wrong policyID": { + ownerID: ag.MFOwnerID, + policyID: wrongPolicyID.String(), + datasetID: dataset.ID, + agentGroup: ag, + err: status.Error(codes.Internal, "internal server error"), + }, + } + + for desc, tc := range cases { + err := commsSVC.NotifyGroupNewDataset(context.Background(), tc.agentGroup, tc.datasetID, tc.policyID, tc.ownerID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", desc, tc.err, err)) + } +} + +func TestNotifyGroupPolicyRemoval(t *testing.T) { + agentGroupRepo := flmocks.NewAgentGroupRepository() + agentRepo := flmocks.NewAgentRepositoryMock() + + commsSVC := newCommsService(agentGroupRepo, agentRepo) + + thingsServer := newThingsServer(newThingsService(users)) + fleetSVC := newFleetService(users, thingsServer.URL, agentGroupRepo, agentRepo) + + agent, err := createAgent(t, "agent", fleetSVC) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + group, err := createAgentGroup(t, "group2", fleetSVC) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + policyID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + cases := map[string]struct { + agent fleet.Agent + agentGroup fleet.AgentGroup + policyID string + policyName string + backend string + err error + }{ + "Notify group policy deletion": { + agent: agent, + agentGroup: group, + policyID: policyID.String(), + policyName: "policy2", + backend: "pktvisor", + err: nil, + }, + } + + for desc, tc := range cases { + err := commsSVC.NotifyGroupPolicyRemoval(tc.agentGroup, tc.policyID, tc.policyName, tc.backend) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", desc, tc.err, err)) + } +} + +func TestNotifyAgentAllDatasets(t *testing.T) { + agentGroupRepo := flmocks.NewAgentGroupRepository() + agentRepo := flmocks.NewAgentRepositoryMock() + + commsSVC := newCommsService(agentGroupRepo, agentRepo) + + thingsServer := newThingsServer(newThingsService(users)) + fleetSVC := newFleetService(users, thingsServer.URL, agentGroupRepo, agentRepo) + + validAgentName, err := types.NewIdentifier("agent2") + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + ag, err := fleetSVC.CreateAgent(context.Background(), "token", fleet.Agent{ + Name: validAgentName, + AgentTags: map[string]string{"test": "true"}, + }) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + validGroupName, err := types.NewIdentifier("group3") + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + group, err := fleetSVC.CreateAgentGroup(context.Background(), "token", fleet.AgentGroup{ + Name: validGroupName, + Tags: map[string]string{"test": "true"}, + }) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + _ = createDataset(t, policiesSVC, "dataset2", group.ID) + + noMatchingGroup, err := createAgent(t, "agent3", fleetSVC) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + cases := map[string]struct { + agent fleet.Agent + err error + }{ + "Notify agent all policies should run": { + agent: ag, + err: nil, + }, + "Notify agent all policies with malformed agent": { + agent: fleet.Agent{MFThingID: ""}, + err: errors.ErrMalformedEntity, + }, + "Notify agent with no matching groups": { + agent: noMatchingGroup, + err: nil, + }, + "Notify agent with wrong thingID": { + agent: fleet.Agent{ + MFOwnerID: ag.MFOwnerID, + MFThingID: wrongID, + MFChannelID: ag.MFChannelID, + AgentTags: ag.AgentTags, + }, + err: fleet.ErrNotFound, + }, + } + + for desc, tc := range cases { + err := commsSVC.NotifyAgentAllDatasets(tc.agent) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", desc, tc.err, err)) + } +} + +func TestNotifyGroupRemoval(t *testing.T) { + agentGroupRepo := flmocks.NewAgentGroupRepository() + agentRepo := flmocks.NewAgentRepositoryMock() + + commsSVC := newCommsService(agentGroupRepo, agentRepo) + + thingsServer := newThingsServer(newThingsService(users)) + fleetSVC := newFleetService(users, thingsServer.URL, agentGroupRepo, agentRepo) + + group, err := createAgentGroup(t, "group4", fleetSVC) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + cases := map[string]struct { + agentGroup fleet.AgentGroup + err error + }{ + "Notify group deletion": { + agentGroup: group, + err: nil, + }, + } + + for desc, tc := range cases { + err := commsSVC.NotifyGroupRemoval(tc.agentGroup) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", desc, tc.err, err)) + } +} + +func TestNotifyGroupPolicyUpdate(t *testing.T) { + agentGroupRepo := flmocks.NewAgentGroupRepository() + agentRepo := flmocks.NewAgentRepositoryMock() + + commsSVC := newCommsService(agentGroupRepo, agentRepo) + + thingsServer := newThingsServer(newThingsService(users)) + fleetSVC := newFleetService(users, thingsServer.URL, agentGroupRepo, agentRepo) + + policy := createPolicy(t, policiesSVC, "policy3") + + agent, err := createAgent(t, "agent4", fleetSVC) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + group, err := createAgentGroup(t, "group6", fleetSVC) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + cases := map[string]struct { + agent fleet.Agent + agentGroup fleet.AgentGroup + policyID string + ownerID string + err error + }{ + "Notify group a policy update": { + agent: agent, + agentGroup: group, + policyID: policy.ID, + ownerID: policy.MFOwnerID, + err: nil, + }, + "Notify group a policy update wih wrong policyID": { + agent: agent, + agentGroup: group, + policyID: wrongID, + ownerID: policy.MFOwnerID, + err: status.Error(codes.Internal, "internal server error"), + }, + } + + for desc, tc := range cases { + err := commsSVC.NotifyGroupPolicyUpdate(context.Background(), tc.agentGroup, tc.policyID, tc.ownerID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", desc, tc.err, err)) + } +} + +func TestNotifyAgentGroupMembership(t *testing.T) { + agentGroupRepo := flmocks.NewAgentGroupRepository() + agentRepo := flmocks.NewAgentRepositoryMock() + + commsSVC := newCommsService(agentGroupRepo, agentRepo) + + thingsServer := newThingsServer(newThingsService(users)) + fleetSVC := newFleetService(users, thingsServer.URL, agentGroupRepo, agentRepo) + + validAgentName, err := types.NewIdentifier("agent5") + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + ag, err := fleetSVC.CreateAgent(context.Background(), "token", fleet.Agent{ + Name: validAgentName, + AgentTags: map[string]string{"test": "true"}, + }) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + validGroupName, err := types.NewIdentifier("group5") + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + _, err = fleetSVC.CreateAgentGroup(context.Background(), "token", fleet.AgentGroup{ + Name: validGroupName, + Tags: map[string]string{"test": "true"}, + }) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + noMatchingGroup, err := createAgent(t, "agent6", fleetSVC) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + cases := map[string]struct { + agent fleet.Agent + err error + }{ + "Notify agent all AgentGroup memberships it belongs to": { + agent: ag, + err: nil, + }, + "Notify agent not belong to any AgentGroup": { + agent: noMatchingGroup, + err: nil, + }, + "Notify agent, but missing thingID": { + agent: fleet.Agent{ + MFOwnerID: ag.MFOwnerID, + MFThingID: "", + MFChannelID: ag.MFChannelID, + AgentTags: ag.AgentTags, + }, + err: fleet.ErrMalformedEntity, + }, + } + + for desc, tc := range cases { + err := commsSVC.NotifyAgentGroupMemberships(tc.agent) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", desc, tc.err, err)) + } +} + +func TestNotifyGroupDatasetRemoval(t *testing.T) { + agentGroupRepo := flmocks.NewAgentGroupRepository() + agentRepo := flmocks.NewAgentRepositoryMock() + + commsSVC := newCommsService(agentGroupRepo, agentRepo) + + thingsServer := newThingsServer(newThingsService(users)) + fleetSVC := newFleetService(users, thingsServer.URL, agentGroupRepo, agentRepo) + + agent, err := createAgent(t, "agent4", fleetSVC) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + group, err := createAgentGroup(t, "group6", fleetSVC) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + policyID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + datasetID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + cases := map[string]struct { + agent fleet.Agent + agentGroup fleet.AgentGroup + dsID string + policyID string + err error + }{ + "Notify group dataset deletion": { + agent: agent, + agentGroup: group, + dsID: datasetID.String(), + policyID: policyID.String(), + err: nil, + }, + } + + for desc, tc := range cases { + err := commsSVC.NotifyGroupDatasetRemoval(tc.agentGroup, tc.dsID, tc.policyID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", desc, tc.err, err)) + } +} + +func TestNotifyAgentStop(t *testing.T) { + agentGroupRepo := flmocks.NewAgentGroupRepository() + agentRepo := flmocks.NewAgentRepositoryMock() + + commsSVC := newCommsService(agentGroupRepo, agentRepo) + + thingsServer := newThingsServer(newThingsService(users)) + fleetSVC := newFleetService(users, thingsServer.URL, agentGroupRepo, agentRepo) + + agent, err := createAgent(t, "agent4", fleetSVC) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + cases := map[string]struct { + channelID string + reason string + err error + }{ + "Notify agent to stop": { + channelID: agent.MFChannelID, + reason: "", + err: nil, + }, + } + + for desc, tc := range cases { + err := commsSVC.NotifyAgentStop(tc.channelID, tc.reason) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", desc, tc.err, err)) + } +} + +func TestNotifyAgentNewGroupMembership(t *testing.T) { + agentGroupRepo := flmocks.NewAgentGroupRepository() + agentRepo := flmocks.NewAgentRepositoryMock() + + commsSVC := newCommsService(agentGroupRepo, agentRepo) + + thingsServer := newThingsServer(newThingsService(users)) + fleetSVC := newFleetService(users, thingsServer.URL, agentGroupRepo, agentRepo) + + validAgentName, err := types.NewIdentifier("agent5") + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + ag, err := fleetSVC.CreateAgent(context.Background(), "token", fleet.Agent{ + Name: validAgentName, + AgentTags: map[string]string{"test": "true"}, + }) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + validGroupName, err := types.NewIdentifier("group5") + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + _, err = fleetSVC.CreateAgentGroup(context.Background(), "token", fleet.AgentGroup{ + Name: validGroupName, + Tags: map[string]string{"test": "true"}, + }) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + cases := map[string]struct { + agent fleet.Agent + agentGroup fleet.AgentGroup + err error + }{ + "Notify agent a new membership of AgentGroup": { + agent: ag, + agentGroup: agentGroup, + err: nil, + }, + } + + for desc, tc := range cases { + err := commsSVC.NotifyAgentNewGroupMembership(tc.agent, tc.agentGroup) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", desc, tc.err, err)) + } +} + +func createPolicy(t *testing.T, svc policies.Service, name string) policies.Policy { + t.Helper() + ID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + + validName, err := types.NewIdentifier(name) + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + + policy := policies.Policy{ + Name: validName, + MFOwnerID: ID.String(), + Description: "An example policy", + Backend: "pktvisor", + Version: 0, + OrbTags: map[string]string{"region": "eu"}, + } + policy_data := `version: "1.0" +visor: + taps: + anycast: + type: pcap + config: + iface: eth0` + + res, err := svc.AddPolicy(context.Background(), token, policy, "yaml", policy_data) + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + return res +} + +func createDataset(t *testing.T, svc policies.Service, name string, groupID string) policies.Dataset { + t.Helper() + ID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + + policyID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + + sinkIDs := make([]string, 2) + for i := 0; i < 2; i++ { + sinkID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + sinkIDs = append(sinkIDs, sinkID.String()) + } + + validName, err := types.NewIdentifier(name) + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + + dataset := policies.Dataset{ + ID: ID.String(), + Name: validName, + PolicyID: policyID.String(), + AgentGroupID: groupID, + SinkIDs: sinkIDs, + } + + res, err := svc.AddDataset(context.Background(), token, dataset) + if err != nil { + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + } + return res +} diff --git a/fleet/comms_types.go b/fleet/comms_types.go index bfc0db5b8..dc2d8c039 100644 --- a/fleet/comms_types.go +++ b/fleet/comms_types.go @@ -53,15 +53,22 @@ type BackendStateInfo struct { } type PolicyStateInfo struct { + Name string `json:"name"` Datasets []string `json:"datasets"` State string `json:"state"` Error string `json:"error,omitempty"` } +type GroupStateInfo struct { + GroupName string `json:"name"` + GroupChannel string `json:"channel"` +} + type Heartbeat struct { SchemaVersion string `json:"schema_version"` TimeStamp time.Time `json:"ts"` State State `json:"state"` BackendState map[string]BackendStateInfo `json:"backend_state"` PolicyState map[string]PolicyStateInfo `json:"policy_state"` + GroupState map[string]GroupStateInfo `json:"group_state"` } diff --git a/fleet/mocks/agent.go b/fleet/mocks/agent.go index 2feb7f372..b33d5992c 100644 --- a/fleet/mocks/agent.go +++ b/fleet/mocks/agent.go @@ -3,6 +3,7 @@ package mocks import ( "context" "github.com/ns1labs/orb/fleet" + "github.com/ns1labs/orb/pkg/errors" "github.com/ns1labs/orb/pkg/types" ) @@ -14,7 +15,12 @@ type agentRepositoryMock struct { } func (a agentRepositoryMock) RetrieveOwnerByChannelID(ctx context.Context, channelID string) (fleet.Agent, error) { - return fleet.Agent{}, nil + for _, ag := range a.agentsMock{ + if ag.MFChannelID == channelID{ + return ag, nil + } + } + return fleet.Agent{}, fleet.ErrNotFound } func (a agentRepositoryMock) RetrieveAgentMetadataByOwner(ctx context.Context, ownerID string) ([]types.Metadata, error) { @@ -47,6 +53,7 @@ func (a agentRepositoryMock) Save(ctx context.Context, agent fleet.Agent) error } } a.agentsMock[agent.MFThingID] = agent + a.counter++ return nil } @@ -66,7 +73,13 @@ func (a agentRepositoryMock) UpdateDataByIDWithChannel(ctx context.Context, agen } func (a agentRepositoryMock) RetrieveByIDWithChannel(ctx context.Context, thingID string, channelID string) (fleet.Agent, error) { - panic("implement me") + if _, ok := a.agentsMock[thingID]; ok { + if a.agentsMock[thingID].MFChannelID != channelID { + return fleet.Agent{}, fleet.ErrNotFound + } + return a.agentsMock[thingID], nil + } + return fleet.Agent{}, fleet.ErrNotFound } func (a agentRepositoryMock) RetrieveAll(ctx context.Context, owner string, pm fleet.PageMetadata) (fleet.Page, error) { @@ -94,7 +107,20 @@ func (a agentRepositoryMock) RetrieveAll(ctx context.Context, owner string, pm f } func (a agentRepositoryMock) RetrieveAllByAgentGroupID(ctx context.Context, owner string, agentGroupID string, onlinishOnly bool) ([]fleet.Agent, error) { - return []fleet.Agent{}, nil + if agentGroupID == "" || owner == "" { + return nil, errors.ErrMalformedEntity + } + + var agents []fleet.Agent + id := uint64(0) + for _, v := range a.agentsMock { + if v.MFOwnerID == owner { + agents = append(agents, v) + } + id++ + } + + return agents, nil } func (a agentRepositoryMock) Delete(ctx context.Context, ownerID, thingID string) error { diff --git a/fleet/mocks/agent_groups.go b/fleet/mocks/agent_groups.go index 06e777bfb..395e36fd7 100644 --- a/fleet/mocks/agent_groups.go +++ b/fleet/mocks/agent_groups.go @@ -46,6 +46,11 @@ func (a *agentGroupRepositoryMock) Save(ctx context.Context, group fleet.AgentGr func (a *agentGroupRepositoryMock) RetrieveAllByAgent(ctx context.Context, agent fleet.Agent) ([]fleet.AgentGroup, error) { var agentGroups []fleet.AgentGroup + + if agent.MFThingID == ""{ + return agentGroups, errors.ErrMalformedEntity + } + for _, v := range a.agentGroupMock { if v.MFOwnerID == agent.MFOwnerID && (reflect.DeepEqual(v.Tags, agent.AgentTags) || reflect.DeepEqual(v.Tags, agent.OrbTags)) { agentGroups = append(agentGroups, v) diff --git a/fleet/mocks/comms.go b/fleet/mocks/comms.go index 15a83a21c..1d52ec444 100644 --- a/fleet/mocks/comms.go +++ b/fleet/mocks/comms.go @@ -3,11 +3,20 @@ package mocks import ( "context" "github.com/ns1labs/orb/fleet" + "reflect" ) var _ fleet.AgentCommsService = (*agentCommsServiceMock)(nil) -type agentCommsServiceMock struct{} +type agentCommsServiceMock struct { + aGroupRepoMock fleet.AgentGroupRepository + aRepoMock fleet.AgentRepository + commsMock map[string][]fleet.Agent +} + +func (ac agentCommsServiceMock) NotifyAgentReset(channelID string, fullReset bool, reason string) error { + return nil +} func (ac agentCommsServiceMock) NotifyAgentStop(MFChannelID string, reason string) error { return nil @@ -25,8 +34,12 @@ func (ac agentCommsServiceMock) NotifyGroupDatasetRemoval(ag fleet.AgentGroup, d return nil } -func NewFleetCommService() fleet.AgentCommsService { - return &agentCommsServiceMock{} +func NewFleetCommService(agentRepo fleet.AgentRepository, agentGroupRepo fleet.AgentGroupRepository) fleet.AgentCommsService { + return &agentCommsServiceMock{ + aRepoMock: agentRepo, + aGroupRepoMock: agentGroupRepo, + commsMock: make(map[string][]fleet.Agent), + } } func (ac agentCommsServiceMock) Start() error { @@ -38,10 +51,38 @@ func (ac agentCommsServiceMock) Stop() error { } func (ac agentCommsServiceMock) NotifyAgentNewGroupMembership(a fleet.Agent, ag fleet.AgentGroup) error { + aGroups, err := ac.aGroupRepoMock.RetrieveAllAgentGroupsByOwner(context.Background(), ag.MFOwnerID, fleet.PageMetadata{Limit: 1}) + if err != nil { + return err + } + + for _, group := range aGroups.AgentGroups{ + if reflect.DeepEqual(group.Tags, a.AgentTags){ + ac.commsMock[group.ID] = append(ac.commsMock[group.ID], a) + } + } + return nil } func (ac agentCommsServiceMock) NotifyAgentGroupMemberships(a fleet.Agent) error { + list, err := ac.aGroupRepoMock.RetrieveAllByAgent(context.Background(), a) + if err != nil { + return err + } + + for _, agentGroup := range list { + agentList, _ := ac.commsMock[agentGroup.ID] + for i, agent := range agentList { + if reflect.DeepEqual(agent.AgentTags, a.AgentTags){ + agentList[i].Name = a.Name + } else { + agentList[i] = agentList[len(agentList)-1] + agentList[len(agentList)-1] = fleet.Agent{} + agentList = agentList[:len(agentList)-1] + } + } + } return nil } diff --git a/fleet/mocks/nats.go b/fleet/mocks/nats.go new file mode 100644 index 000000000..c1c6ce4a3 --- /dev/null +++ b/fleet/mocks/nats.go @@ -0,0 +1,32 @@ +package mocks + +import ( + "github.com/mainflux/mainflux/logger" + "github.com/mainflux/mainflux/pkg/messaging" + "github.com/mainflux/mainflux/pkg/messaging/nats" +) + +var _ nats.PubSub = (*pubSubMock)(nil) + +type pubSubMock struct { + +} + +func NewPubSub(url, queue string, logger logger.Logger) (nats.PubSub, error) { + return &pubSubMock{}, nil +} + +func (p pubSubMock) Publish(topic string, msg messaging.Message) error { + return nil +} + +func (p pubSubMock) Subscribe(topic string, handler messaging.MessageHandler) error { + return nil +} + +func (p pubSubMock) Unsubscribe(topic string) error { + return nil +} + +func (p pubSubMock) Close() { +} diff --git a/fleet/postgres/agent_groups_test.go b/fleet/postgres/agent_groups_test.go index c1fecedae..53c5a416a 100644 --- a/fleet/postgres/agent_groups_test.go +++ b/fleet/postgres/agent_groups_test.go @@ -121,6 +121,16 @@ func TestAgentGroupRetrieve(t *testing.T) { groupID: group.MFOwnerID, err: errors.ErrNotFound, }, + "retrieve agent group with empty owner": { + ownerID: "", + groupID: id, + err: errors.ErrMalformedEntity, + }, + "retrieve agent group with empty groupID": { + ownerID: group.MFOwnerID, + groupID: "", + err: errors.ErrMalformedEntity, + }, } for desc, tc := range cases { @@ -233,6 +243,16 @@ func TestMultiAgentGroupRetrieval(t *testing.T) { }, size: n, }, + "retrieve all groups filtered by tags": { + owner: oID.String(), + pageMetadata: fleet.PageMetadata{ + Offset: 0, + Limit: n, + Total: n, + Tags: types.Tags{"testkey": "testvalue"}, + }, + size: n, + }, } for desc, tc := range cases { @@ -310,6 +330,13 @@ func TestAgentGroupUpdate(t *testing.T) { }, err: fleet.ErrNotFound, }, + "update existing agent by thingID with invalid ownerID": { + group: fleet.AgentGroup{ + ID: groupID, + MFOwnerID: "123", + }, + err: errors.ErrMalformedEntity, + }, } for desc, tc := range cases { @@ -376,6 +403,90 @@ func TestAgentGroupDelete(t *testing.T) { } } +func TestAgentGroupRetrieveAllByAgent(t *testing.T) { + dbMiddleware := postgres.NewDatabase(db) + agentGroupRepo := postgres.NewAgentGroupRepository(dbMiddleware, logger) + agentRepo := postgres.NewAgentRepository(db, logger) + + oID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + chID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + nameID, err := types.NewIdentifier("myagent") + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + thID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + wrongID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + agent := fleet.Agent{ + Name: nameID, + MFThingID: thID.String(), + MFOwnerID: oID.String(), + MFChannelID: chID.String(), + OrbTags: types.Tags{"testkey": "testvalue"}, + AgentTags: types.Tags{"testkey": "testvalue"}, + LastHBData: types.Metadata{"heartbeatdata": "testvalue"}, + } + + err = agentRepo.Save(context.Background(), agent) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err)) + + n := uint64(10) + for i := uint64(0); i < n; i++ { + + nameID, err := types.NewIdentifier(fmt.Sprintf("ue-agent-group-%d", i)) + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + group := fleet.AgentGroup{ + Name: nameID, + Description: "a example", + MFOwnerID: oID.String(), + MFChannelID: chID.String(), + Tags: types.Tags{"testkey": "testvalue"}, + } + + ag, err := agentGroupRepo.Save(context.Background(), group) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err)) + fmt.Sprint(ag) + } + + cases := map[string]struct { + agent fleet.Agent + size uint64 + }{ + "retrieve all groups with existing owner": { + agent: agent, + size: n, + }, + "retrieve all groups with non-existing agent": { + agent: fleet.Agent{ + Name: nameID, + MFThingID: wrongID.String(), + MFOwnerID: oID.String(), + MFChannelID: chID.String(), + OrbTags: types.Tags{"testkey": "testvalue"}, + AgentTags: types.Tags{"testkey": "testvalue"}, + LastHBData: types.Metadata{"heartbeatdata": "testvalue"}, + }, + size: 0, + }, + } + + for desc, tc := range cases { + t.Run(desc, func(t *testing.T) { + groups, err := agentGroupRepo.RetrieveAllByAgent(context.Background(), tc.agent) + require.Nil(t, err, fmt.Sprintf("%s: unexpected error: %s\n", desc, err)) + size := uint64(len(groups)) + assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected size %d got %d", desc, tc.size, size)) + }) + } +} + func testSortAgentGroups(t *testing.T, pm fleet.PageMetadata, ags []fleet.AgentGroup) { switch pm.Order { case "name": diff --git a/fleet/postgres/agents.go b/fleet/postgres/agents.go index 1d1820464..5cba705c2 100644 --- a/fleet/postgres/agents.go +++ b/fleet/postgres/agents.go @@ -31,19 +31,23 @@ type agentRepository struct { } func (r agentRepository) RetrieveMatchingAgents(ctx context.Context, ownerID string, tags types.Tags) (types.Metadata, error) { - t, tmq, err := getOrbOrAgentTagsQuery(tags) + t, tmq, err := getTagsQuery(tags) if err != nil { return types.Metadata{}, errors.Wrap(errors.ErrSelectEntity, err) } q := fmt.Sprintf( `select - json_build_object('total', coalesce(total,0), 'online', coalesce(online,0)) AS matching_agents + json_build_object('total', sum(coalesce(total,0)), 'online', sum(coalesce(online,0))) AS matching_agents from (select + mf_owner_id, + coalesce(agent_tags || orb_tags, agent_tags, orb_tags) as tags, sum(case when mf_thing_id is not null then 1 else 0 end) as total, sum(case when state = 'online' then 1 else 0 end) as online - from agents WHERE mf_owner_id = :mf_owner_id %s) as agent_groups`, tmq) + from agents where mf_owner_id = :mf_owner_id + group by mf_owner_id, coalesce(agent_tags || orb_tags, agent_tags, orb_tags)) agent_groups + WHERE 1=1 %s`, tmq) params := map[string]interface{}{ "tags": t, @@ -116,13 +120,22 @@ func (r agentRepository) RetrieveAll(ctx context.Context, owner string, pm fleet if err != nil { return fleet.Page{}, errors.Wrap(errors.ErrSelectEntity, err) } - t, tmq, err := getOrbOrAgentTagsQuery(pm.Tags) + t, tmq, err := getTagsQuery(pm.Tags) if err != nil { return fleet.Page{}, errors.Wrap(errors.ErrSelectEntity, err) } q := fmt.Sprintf(`SELECT mf_thing_id, name, mf_owner_id, mf_channel_id, ts_created, orb_tags, agent_tags, agent_metadata, state, last_hb_data, ts_last_hb - FROM agents WHERE mf_owner_id = :mf_owner_id %s%s%s ORDER BY %s %s LIMIT :limit OFFSET :offset;`, tmq, mq, nq, oq, dq) + from ( + select + mf_thing_id, name, mf_owner_id, mf_channel_id, ts_created, orb_tags, agent_tags, agent_metadata, state, last_hb_data, ts_last_hb, + coalesce(agent_tags || orb_tags, agent_tags, orb_tags) as tags + from agents where mf_owner_id = :mf_owner_id + group by + mf_thing_id, name, mf_owner_id, mf_channel_id, ts_created, orb_tags, agent_tags, agent_metadata, state, last_hb_data, ts_last_hb, + coalesce(agent_tags || orb_tags, agent_tags, orb_tags)) as agts + WHERE 1=1 %s%s%s + ORDER BY %s %s LIMIT :limit OFFSET :offset;`, tmq, mq, nq, oq, dq) params := map[string]interface{}{ "mf_owner_id": owner, "limit": pm.Limit, @@ -153,7 +166,35 @@ func (r agentRepository) RetrieveAll(ctx context.Context, owner string, pm fleet items = append(items, th) } - cq := fmt.Sprintf(`SELECT COUNT(*) FROM agents WHERE mf_owner_id = :mf_owner_id %s%s%s;`, nq, tmq, mq) + cq := fmt.Sprintf(`SELECT count(*) + from ( + select + mf_thing_id, + name, + mf_owner_id, + mf_channel_id, + ts_created, + orb_tags, + agent_tags, + agent_metadata, + state, + last_hb_data, + ts_last_hb, + coalesce(agent_tags || orb_tags, agent_tags, orb_tags) as tags + from agents where mf_owner_id = :mf_owner_id + group by mf_thing_id, + name, + mf_owner_id, + mf_channel_id, + ts_created, + orb_tags, + agent_tags, + agent_metadata, + state, + last_hb_data, + ts_last_hb, + coalesce(agent_tags || orb_tags, agent_tags, orb_tags)) as agts + WHERE 1=1 %s%s%s;`, nq, tmq, mq) total, err := total(ctx, r.db, cq, params) if err != nil { diff --git a/fleet/postgres/agents_test.go b/fleet/postgres/agents_test.go index 147aab46b..5946ef8a8 100644 --- a/fleet/postgres/agents_test.go +++ b/fleet/postgres/agents_test.go @@ -20,6 +20,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" + "reflect" "strings" "testing" ) @@ -85,6 +86,42 @@ func TestAgentSave(t *testing.T) { agent: agentCopy, err: errors.ErrConflict, }, + "create new agent with empty OwnerID": { + agent: fleet.Agent{ + Name: nameID, + MFOwnerID: "", + MFThingID: thID.String(), + MFChannelID: chID.String(), + }, + err: errors.ErrMalformedEntity, + }, + "create new agent with empty ThingID": { + agent: fleet.Agent{ + Name: nameID, + MFOwnerID: oID.String(), + MFThingID: "", + MFChannelID: chID.String(), + }, + err: errors.ErrMalformedEntity, + }, + "create new agent with empty channelID": { + agent: fleet.Agent{ + Name: nameID, + MFOwnerID: oID.String(), + MFThingID: thID.String(), + MFChannelID: "", + }, + err: errors.ErrMalformedEntity, + }, + "create new agent with empty invalid OwnerID": { + agent: fleet.Agent{ + Name: nameID, + MFOwnerID: "123", + MFThingID: thID.String(), + MFChannelID: chID.String(), + }, + err: errors.ErrMalformedEntity, + }, } for desc, tc := range cases { @@ -207,6 +244,22 @@ func TestAgentUpdateData(t *testing.T) { }, err: errors.ErrNotFound, }, + "update agent data by thingID and channelID with invalid thingID": { + agent: fleet.Agent{ + MFThingID: "123", + MFChannelID: chID.String(), + AgentMetadata: types.Metadata{"newkey": "newvalue"}, + }, + err: errors.ErrMalformedEntity, + }, + "update agent data by thingID with channelID with empty fields": { + agent: fleet.Agent{ + MFThingID: "", + MFChannelID: "", + AgentMetadata: types.Metadata{"newkey": "newvalue"}, + }, + err: errors.ErrMalformedEntity, + }, } for desc, tc := range cases { @@ -269,6 +322,22 @@ func TestAgentUpdateHeartbeat(t *testing.T) { }, err: errors.ErrNotFound, }, + "update existing agent heartbeat with empty thingID and channelID": { + agent: fleet.Agent{ + MFThingID: "", + MFChannelID: "", + LastHBData: types.Metadata{"heartbeatdata2": "newvalue"}, + }, + err: errors.ErrMalformedEntity, + }, + "update existing agent heartbeat with invalid thingID": { + agent: fleet.Agent{ + MFThingID: "123", + MFChannelID: chID.String(), + LastHBData: types.Metadata{"heartbeatdata2": "newvalue"}, + }, + err: errors.ErrMalformedEntity, + }, } for desc, tc := range cases { @@ -296,8 +365,9 @@ func TestMultiAgentRetrieval(t *testing.T) { name := "agent_name" metaStr := `{"field1":"value1","field2":{"subfield11":"value2","subfield12":{"subfield121":"value3","subfield122":"value4"}}}` subMetaStr := `{"field2":{"subfield12":{"subfield121":"value3"}}}` - tagsStr := `{"region": "EU", "node_type": "dns"}` + tagsStr := `{"node_type": "dns"}` subTagsStr := `{"region": "EU"}` + mixTagsStr := `{"node_type": "dns", "region": "EU"}` metadata := types.Metadata{} json.Unmarshal([]byte(metaStr), &metadata) @@ -311,6 +381,9 @@ func TestMultiAgentRetrieval(t *testing.T) { subTags := types.Tags{} json.Unmarshal([]byte(subTagsStr), &subTags) + mixTags := types.Tags{} + json.Unmarshal([]byte(mixTagsStr), &mixTags) + wrongMeta := types.Metadata{ "field": "value1", } @@ -445,6 +518,16 @@ func TestMultiAgentRetrieval(t *testing.T) { }, size: n, }, + "retrieve agents with mix tags": { + owner: oID.String(), + pageMetadata: fleet.PageMetadata{ + Offset: 0, + Limit: n, + Total: n, + Tags: mixTags, + }, + size: n, + }, } for desc, tc := range cases { @@ -470,6 +553,9 @@ func TestAgentUpdate(t *testing.T) { thID, err := uuid.NewV4() require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + duplicatedThID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + chID, err := uuid.NewV4() require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) @@ -482,6 +568,9 @@ func TestAgentUpdate(t *testing.T) { updatedNameID, err := types.NewIdentifier("my-agent2") require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + duplicatedNameID, err := types.NewIdentifier("my-agent3") + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + agent := fleet.Agent{ Name: nameID, MFThingID: thID.String(), @@ -492,9 +581,22 @@ func TestAgentUpdate(t *testing.T) { AgentMetadata: types.Metadata{"testkey": "testvalue"}, } + duplicatedAgent := fleet.Agent{ + Name: duplicatedNameID, + MFThingID: duplicatedThID.String(), + MFOwnerID: oID.String(), + MFChannelID: chID.String(), + OrbTags: types.Tags{"testkey": "testvalue"}, + AgentTags: types.Tags{"testkey": "testvalue"}, + AgentMetadata: types.Metadata{"testkey": "testvalue"}, + } + err = agentRepo.Save(context.Background(), agent) require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err)) + err = agentRepo.Save(context.Background(), duplicatedAgent) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err)) + cases := map[string]struct { agent fleet.Agent err error @@ -517,6 +619,41 @@ func TestAgentUpdate(t *testing.T) { }, err: errors.ErrNotFound, }, + "update agent data with empty thingID": { + agent: fleet.Agent{ + MFThingID: "", + MFOwnerID: oID.String(), + Name: updatedNameID, + OrbTags: types.Tags{"newkey": "newvalue"}, + }, + err: errors.ErrMalformedEntity, + }, + "update agent data with empty OwnerID": { + agent: fleet.Agent{ + MFThingID: thID.String(), + MFOwnerID: "", + Name: updatedNameID, + OrbTags: types.Tags{"newkey": "newvalue"}, + }, + err: errors.ErrMalformedEntity, + }, + "update agent data by thingID and channelID with invalid thingID": { + agent: fleet.Agent{ + MFThingID: "123", + MFOwnerID: oID.String(), + Name: updatedNameID, + AgentMetadata: types.Metadata{"newkey": "newvalue"}, + }, + err: errors.ErrMalformedEntity, + }, + "update agent data by thingID and channelID with duplicated nameID": { + agent: fleet.Agent{ + MFThingID: thID.String(), + MFOwnerID: oID.String(), + Name: duplicatedNameID, + }, + err: errors.ErrConflict, + }, } for desc, tc := range cases { @@ -645,6 +782,246 @@ func TestAgentBackendTapsRetrieve(t *testing.T) { } } +func TestMultiAgentRetrievalByAgentGroup(t *testing.T) { + dbMiddleware := postgres.NewDatabase(db) + agentRepo := postgres.NewAgentRepository(dbMiddleware, logger) + agentGroupRepo := postgres.NewAgentGroupRepository(dbMiddleware, logger) + + oID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + wrongoID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + groupNameID, err := types.NewIdentifier("my-group") + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + chID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + metaStr := `{"field1":"value1","field2":{"subfield11":"value2","subfield12":{"subfield121":"value3","subfield122":"value4"}}}` + metadata := types.Metadata{} + json.Unmarshal([]byte(metaStr), &metadata) + + tagsStr := `{"region": "EU", "node_type": "dns"}` + tags := types.Tags{} + json.Unmarshal([]byte(tagsStr), &tags) + + subTagsStr := `{"region": "EU"}` + subTags := types.Tags{} + json.Unmarshal([]byte(subTagsStr), &subTags) + + + group := fleet.AgentGroup{ + Name: groupNameID, + MFOwnerID: oID.String(), + MFChannelID: chID.String(), + Tags: tags, + } + + id, err := agentGroupRepo.Save(context.Background(), group) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err)) + + n := uint64(10) + for i := uint64(0); i < n; i++ { + thID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + chID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + th := fleet.Agent{ + MFOwnerID: oID.String(), + MFThingID: thID.String(), + MFChannelID: chID.String(), + } + + th.Name, err = types.NewIdentifier(fmt.Sprintf("agent_name-%d", i)) + require.True(t, th.Name.IsValid(), "invalid Identifier name: %s") + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + th.AgentMetadata = metadata + th.AgentTags = tags + th.OrbTags = subTags + th.State = fleet.Online + + err = agentRepo.Save(context.Background(), th) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err)) + } + + cases := map[string]struct { + owner string + groupID string + onlinish bool + size uint64 + err error + }{ + "retrieve all agents with existing owner that are online": { + owner: oID.String(), + onlinish: true, + groupID: id, + size: n, + err: nil, + }, + "retrieve all agents with empty owner": { + owner: "", + onlinish: true, + groupID: id, + size: 0, + err: errors.ErrMalformedEntity, + }, + "retrieve agents with non-existing groupID": { + owner: oID.String(), + groupID: wrongoID.String(), + onlinish: true, + size: 0, + err: nil, + }, + "retrieve agents with non-existing owner": { + owner: wrongoID.String(), + groupID: id, + onlinish: true, + size: 0, + err: nil, + }, + } + + for desc, tc := range cases { + t.Run(desc, func(t *testing.T) { + agents, err := agentRepo.RetrieveAllByAgentGroupID(context.Background(), tc.owner, tc.groupID, tc.onlinish) + size := uint64(len(agents)) + assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.size, size)) + assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %d\n", desc, tc.err, err)) + + }) + } +} + +func TestAgentRetrieveByID(t *testing.T) { + dbMiddleware := postgres.NewDatabase(db) + agentRepo := postgres.NewAgentRepository(dbMiddleware, logger) + + thID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + chID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + oID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + nameID, err := types.NewIdentifier("myagent") + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + agent := fleet.Agent{ + Name: nameID, + MFThingID: thID.String(), + MFOwnerID: oID.String(), + MFChannelID: chID.String(), + OrbTags: types.Tags{"testkey": "testvalue"}, + AgentTags: types.Tags{"testkey": "testvalue"}, + AgentMetadata: types.Metadata{"testkey": "testvalue"}, + } + + err = agentRepo.Save(context.Background(), agent) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err)) + + cases := map[string]struct { + thingID string + ownerID string + err error + tags types.Tags + }{ + "retrieve existing agent by thingID": { + thingID: thID.String(), + ownerID: oID.String(), + tags: types.Tags{"testkey": "testvalue"}, + err: nil, + }, + "retrieve non-existent agent by thingID": { + thingID: thID.String(), + ownerID: thID.String(), + err: errors.ErrNotFound, + }, + "retrieve existing agent by thingID with invalid ownerID": { + thingID: thID.String(), + ownerID: "123", + err: errors.ErrNotFound, + }, + } + + for desc, tc := range cases { + t.Run(desc, func(t *testing.T) { + ag, err := agentRepo.RetrieveByID(context.Background(), tc.ownerID, tc.thingID) + if err == nil { + assert.Equal(t, nameID, ag.Name, fmt.Sprintf("%s: expected %s got %s\n", desc, nameID, ag.Name)) + } + if len(tc.tags) > 0 { + assert.Equal(t, tc.tags, ag.OrbTags) + assert.Equal(t, tc.tags, ag.AgentTags) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err)) + }) + } +} + +func TestRetrieveOwnerByChannelID(t *testing.T) { + dbMiddleware := postgres.NewDatabase(db) + agentRepo := postgres.NewAgentRepository(dbMiddleware, logger) + + thID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + chID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + oID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + nameID, err := types.NewIdentifier("myagent") + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + agent := fleet.Agent{ + Name: nameID, + MFThingID: thID.String(), + MFOwnerID: oID.String(), + MFChannelID: chID.String(), + OrbTags: types.Tags{"testkey": "testvalue"}, + AgentTags: types.Tags{"testkey": "testvalue"}, + AgentMetadata: types.Metadata{"testkey": "testvalue"}, + } + + err = agentRepo.Save(context.Background(), agent) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err)) + + cases := map[string]struct { + channelID string + ownerID string + name string + err error + }{ + "retrieve existing owner by channelID": { + channelID: chID.String(), + ownerID: oID.String(), + name: nameID.String(), + err: nil, + }, + "retrieve existent owner by non-existent channelID": { + channelID: thID.String(), + ownerID: "", + name: "", + err: nil, + }, + } + + for desc, tc := range cases { + t.Run(desc, func(t *testing.T) { + ag, err := agentRepo.RetrieveOwnerByChannelID(context.Background(), tc.channelID) + if err == nil { + assert.Equal(t, tc.name, ag.Name.String(), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.name, ag.Name.String())) + assert.Equal(t, tc.ownerID, ag.MFOwnerID, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.ownerID, ag.MFOwnerID)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err)) + }) + } +} + func testSortAgents(t *testing.T, pm fleet.PageMetadata, ths []fleet.Agent) { switch pm.Order { case "name": @@ -662,3 +1039,109 @@ func testSortAgents(t *testing.T, pm fleet.PageMetadata, ths []fleet.Agent) { break } } + +func TestMatchingAgentRetrieval(t *testing.T) { + dbMiddleware := postgres.NewDatabase(db) + agentRepo := postgres.NewAgentRepository(dbMiddleware, logger) + + oID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + name := "agent_name" + orbTagsStr := `{"node_type": "dns"}` + agentTagsStr := `{"region": "EU"}` + mixTagsStr := `{"node_type": "dns", "region": "EU"}` + + + orbTags := types.Tags{} + json.Unmarshal([]byte(orbTagsStr), &orbTags) + + agentTags := types.Tags{} + json.Unmarshal([]byte(agentTagsStr), &agentTags) + + mixTags := types.Tags{} + json.Unmarshal([]byte(mixTagsStr), &mixTags) + + n := uint64(3) + for i := uint64(0); i < n; i++ { + thID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + chID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + th := fleet.Agent{ + MFOwnerID: oID.String(), + MFThingID: thID.String(), + MFChannelID: chID.String(), + } + + th.Name, err = types.NewIdentifier(fmt.Sprintf("%s-%d", name, i)) + require.True(t, th.Name.IsValid(), "invalid Identifier name: %s") + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + th.AgentTags = agentTags + th.OrbTags = orbTags + + err = agentRepo.Save(context.Background(), th) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err)) + } + + cases := map[string]struct { + owner string + tag types.Tags + matchingAgents types.Metadata + }{ + "retrieve matching agents with mix tags": { + owner: oID.String(), + tag: mixTags, + matchingAgents: types.Metadata{ + "total": float64(n), + "online": float64(0), + }, + }, + "retrieve matching agents with orb tags": { + owner: oID.String(), + tag: orbTags, + matchingAgents: types.Metadata{ + "total": float64(n), + "online": float64(0), + }, + }, + "retrieve matching agents with agent tags": { + owner: oID.String(), + tag: agentTags, + matchingAgents: types.Metadata{ + "total": float64(n), + "online": float64(0), + }, + }, + "retrieve unmatched agents with mix tags": { + owner: oID.String(), + tag: types.Tags{ + "wrong": "tag", + }, + matchingAgents: types.Metadata{ + "total": nil, + "online": nil, + }, + }, + "retrieve agents with mix tags": { + owner: oID.String(), + tag: types.Tags{ + "node_type": "dns", + "region": "EU", + "wrong": "tag", + }, + matchingAgents: types.Metadata{ + "total": nil, + "online": nil, + }, + }, + } + + for desc, tc := range cases { + t.Run(desc, func(t *testing.T) { + ma, err := agentRepo.RetrieveMatchingAgents(context.Background(), tc.owner, tc.tag) + assert.True(t, reflect.DeepEqual(tc.matchingAgents, ma), fmt.Sprintf("%s: expected %v got %v\n", desc, tc.matchingAgents, ma)) + assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err)) + }) + } +} diff --git a/fleet/redis/producer/streams.go b/fleet/redis/producer/streams.go index 33c605530..d5911c207 100644 --- a/fleet/redis/producer/streams.go +++ b/fleet/redis/producer/streams.go @@ -28,6 +28,10 @@ type eventStore struct { logger *zap.Logger } +func (es eventStore) ResetAgent(ct context.Context, token string, agentID string) error { + return es.svc.ResetAgent(ct, token, agentID) +} + func (es eventStore) ViewOwnerByChannelIDInternal(ctx context.Context, channelID string) (fleet.Agent, error) { return es.svc.ViewOwnerByChannelIDInternal(ctx, channelID) } diff --git a/policies/api/http/endpoint.go b/policies/api/http/endpoint.go index 881e0c610..27f79534c 100644 --- a/policies/api/http/endpoint.go +++ b/policies/api/http/endpoint.go @@ -78,6 +78,7 @@ func viewPolicyEndpoint(svc policies.Service) endpoint.Endpoint { SchemaVersion: policy.SchemaVersion, Policy: policy.Policy, Version: policy.Version, + LastModified: policy.LastModified, } return res, nil } @@ -113,6 +114,7 @@ func listPoliciesEndpoint(svc policies.Service) endpoint.Endpoint { Version: ag.Version, Backend: ag.Backend, SchemaVersion: ag.SchemaVersion, + LastModified: ag.LastModified, } res.Policies = append(res.Policies, view) } diff --git a/policies/api/http/endpoint_test.go b/policies/api/http/endpoint_test.go index 80fef33f6..fe954be5d 100644 --- a/policies/api/http/endpoint_test.go +++ b/policies/api/http/endpoint_test.go @@ -346,6 +346,12 @@ func TestPolicyEdition(t *testing.T) { cli := newClientServer(t) policy := createPolicy(t, &cli, "policy") + var ( + invalidNamePolicyJson = "{\"name\": \"*#simple_dns#*\", \"backend\": \"pktvisor\", \"policy\": { \"kind\": \"collection\", \"input\": {\"tap\": \"mydefault\", \"input_type\": \"pcap\"}, \"handlers\": {\"modules\": {\"default_net\": {\"type\": \"net\"}, \"default_dns\": {\"type\": \"dns\"}}}}}" + emptyFormatPolicyYaml = `{"name": "mypktvisorpolicyyaml-3", "backend": "pktvisor", "description": "my pktvisor policy yaml", "tags": {"region": "eu", "node_type": "dns"}, "format": "","policy_data": "version: \"1.0\"\nvisor:\n handlers:\n modules:\n default_dns:\n type: dns\n default_net:\n type: net\ninput:\n input_type: pcap\n tap: mydefault\nkind: collection"}` + notEmptyFormatPolicyJson = "{\"name\": \"simple_dns\", \"backend\": \"pktvisor\", \"format\": \"json\", \"policy\": { \"kind\": \"collection\", \"input\": {\"tap\": \"mydefault\", \"input_type\": \"pcap\"}, \"handlers\": {\"modules\": {\"default_net\": {\"type\": \"net\"}, \"default_dns\": {\"type\": \"dns\"}}}}}" + ) + cases := map[string]struct { id string contentType string @@ -423,6 +429,27 @@ func TestPolicyEdition(t *testing.T) { auth: token, status: http.StatusBadRequest, }, + "update a existing policy with a invalid name": { + id: policy.ID, + contentType: "application/json", + auth: token, + status: http.StatusBadRequest, + data: invalidNamePolicyJson, + }, + "update a policy with invalid yaml - empty Format field": { + id: policy.ID, + contentType: contentType, + auth: token, + status: http.StatusBadRequest, + data: emptyFormatPolicyYaml, + }, + "update a policy with invalid json - not empty Format field": { + id: policy.ID, + contentType: contentType, + auth: token, + status: http.StatusBadRequest, + data: notEmptyFormatPolicyJson, + }, } for desc, tc := range cases { @@ -599,6 +626,11 @@ func TestCreatePolicy(t *testing.T) { cli := newClientServer(t) defer cli.server.Close() + var ( + emptyFormatPolicyYaml = `{"name": "mypktvisorpolicyyaml-3", "backend": "pktvisor", "description": "my pktvisor policy yaml", "tags": {"region": "eu", "node_type": "dns"}, "format": "","policy_data": "version: \"1.0\"\nvisor:\n handlers:\n modules:\n default_dns:\n type: dns\n default_net:\n type: net\ninput:\n input_type: pcap\n tap: mydefault\nkind: collection"}` + notEmptyFormatPolicyJson = "{\"name\": \"simple_dns\", \"backend\": \"pktvisor\", \"format\": \"json\", \"policy\": { \"kind\": \"collection\", \"input\": {\"tap\": \"mydefault\", \"input_type\": \"pcap\"}, \"handlers\": {\"modules\": {\"default_net\": {\"type\": \"net\"}, \"default_dns\": {\"type\": \"dns\"}}}}}" + ) + // Conflict scenario createPolicy(t, &cli, "conflict-simple_dns") @@ -651,6 +683,20 @@ func TestCreatePolicy(t *testing.T) { status: http.StatusUnsupportedMediaType, location: "/policies/agent", }, + "add a policy with invalid yaml - empty Format field": { + req: emptyFormatPolicyYaml, + contentType: contentType, + auth: token, + status: http.StatusBadRequest, + location: "/policies/agent", + }, + "add a policy with invalid json - not empty Format field": { + req: notEmptyFormatPolicyJson, + contentType: contentType, + auth: token, + status: http.StatusBadRequest, + location: "/policies/agent", + }, } for desc, tc := range cases { @@ -674,6 +720,14 @@ func TestCreateDataset(t *testing.T) { cli := newClientServer(t) defer cli.server.Close() + var emptyGroupIDDatasetJson = `{ + "name": "my-dataset", + "agent_group_id": "", + "agent_policy_id": "bfa9351d-8075-444f-9a4c-228f9a476a0d", + "sink_ids": ["03679425-aa69-4574-bf62-e0fe71b80939", "03679425-aa69-4574-bf62-e0fe71b80939"], + "tags":{} +}` + // Conflict scenario createDataset(t, &cli, "my-dataset-conflict") @@ -719,6 +773,13 @@ func TestCreateDataset(t *testing.T) { status: http.StatusUnsupportedMediaType, location: "/policies/dataset", }, + "add a dataset with empty GroupID field": { + req: emptyGroupIDDatasetJson, + contentType: contentType, + auth: token, + status: http.StatusBadRequest, + location: "/policies/dataset", + }, } for desc, tc := range cases { @@ -812,6 +873,13 @@ func TestDatasetEdition(t *testing.T) { auth: token, status: http.StatusBadRequest, }, + "update a dataset with empty ID requisition": { + id: "", + contentType: "application/json", + auth: token, + status: http.StatusBadRequest, + data: validDatasetJson, + }, } for desc, tc := range cases { @@ -1021,6 +1089,11 @@ func TestViewDataset(t *testing.T) { token: "", status: http.StatusUnauthorized, }, + "view a dataset with empty ID requisition": { + ID: "", + token: token, + status: http.StatusBadRequest, + }, } for desc, tc := range cases { diff --git a/policies/api/http/requests.go b/policies/api/http/requests.go index 4e71b6012..75794373e 100644 --- a/policies/api/http/requests.go +++ b/policies/api/http/requests.go @@ -35,7 +35,7 @@ type addPolicyReq struct { token string } -func (req addPolicyReq) validate() error { +func (req *addPolicyReq) validate() error { if req.token == "" { return errors.ErrUnauthorizedAccess } diff --git a/policies/api/http/responses.go b/policies/api/http/responses.go index e62027e69..22bd8108a 100644 --- a/policies/api/http/responses.go +++ b/policies/api/http/responses.go @@ -21,6 +21,7 @@ type policyRes struct { Format string `json:"format,omitempty"` PolicyData string `json:"policy_data,omitempty"` Version int32 `json:"version,omitempty"` + LastModified time.Time `json:"ts_last_modified"` created bool } diff --git a/policies/mocks/policies.go b/policies/mocks/policies.go index 34b89d026..0896ae687 100644 --- a/policies/mocks/policies.go +++ b/policies/mocks/policies.go @@ -43,7 +43,14 @@ func (m *mockPoliciesRepository) UpdateDataset(ctx context.Context, ownerID stri } func (m *mockPoliciesRepository) InactivateDatasetByPolicyID(ctx context.Context, policyID string, ownerID string) error { - //todo implement when create unit tests to dataset + if ds, ok := m.ddb[policyID]; ok { + if m.ddb[policyID].MFOwnerID != ownerID { + return policies.ErrUpdateEntity + } + ds.Valid = false + return nil + } + return nil } @@ -136,8 +143,13 @@ func (m *mockPoliciesRepository) RetrievePolicyByID(ctx context.Context, policyI } func (m *mockPoliciesRepository) RetrievePoliciesByGroupID(ctx context.Context, groupIDs []string, ownerID string) (ret []policies.PolicyInDataset, err error) { + if len(groupIDs) == 0 || ownerID == "" { + return nil, errors.ErrMalformedEntity + } + for _, d := range groupIDs { - ret = append(ret, m.gdb[d][0]) + ret = make([]policies.PolicyInDataset, len(m.gdb[d])) + copy(ret, m.gdb[d]) } return ret, nil } @@ -152,8 +164,13 @@ func (m *mockPoliciesRepository) SaveDataset(ctx context.Context, dataset polici ID, _ := uuid.NewV4() dataset.ID = ID.String() m.ddb[dataset.ID] = dataset - m.gdb[dataset.AgentGroupID] = make([]policies.PolicyInDataset, 1) - m.gdb[dataset.AgentGroupID][0] = policies.PolicyInDataset{Policy: m.pdb[dataset.PolicyID], DatasetID: dataset.ID} + + if _, ok := m.gdb[dataset.AgentGroupID]; !ok { + m.gdb[dataset.AgentGroupID] = make([]policies.PolicyInDataset, 1) + m.gdb[dataset.AgentGroupID][0] = policies.PolicyInDataset{Policy: m.pdb[dataset.PolicyID], DatasetID: dataset.ID} + } else { + m.gdb[dataset.AgentGroupID] = append(m.gdb[dataset.AgentGroupID], policies.PolicyInDataset{Policy: m.pdb[dataset.PolicyID], DatasetID: dataset.ID}) + } m.dataSetCounter++ return ID.String(), nil } @@ -169,7 +186,14 @@ func (m *mockPoliciesRepository) RetrieveDatasetByID(ctx context.Context, datase } func (m *mockPoliciesRepository) InactivateDatasetByGroupID(ctx context.Context, groupID string, ownerID string) error { - panic("implement me") + for _, ds := range m.ddb{ + if ds.AgentGroupID == groupID && ds.MFOwnerID == ownerID{ + ds.Valid = false + return nil + } + } + + return policies.ErrNotFound } func (m *mockPoliciesRepository) RetrieveAllDatasetsByOwner(ctx context.Context, owner string, pm policies.PageMetadata) (policies.PageDataset, error) { diff --git a/policies/policies.go b/policies/policies.go index b765d936b..a27abe826 100644 --- a/policies/policies.go +++ b/policies/policies.go @@ -21,6 +21,7 @@ type Policy struct { OrbTags types.Tags Policy types.Metadata Created time.Time + LastModified time.Time } type Dataset struct { diff --git a/policies/policy_service_test.go b/policies/policy_service_test.go index 953ffdb7c..0c86126de 100644 --- a/policies/policy_service_test.go +++ b/policies/policy_service_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "testing" + "time" ) const ( @@ -724,6 +725,329 @@ func TestListDataset(t *testing.T) { } } +func TestListPoliciesByGroupIDInternal(t *testing.T) { + users := flmocks.NewAuthService(map[string]string{token: email}) + svc := newService(users) + + agentGroupID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + + policy := createPolicy(t, svc, "policy") + + var total = 10 + + datasetsID := make([]string, total) + + for i := 0; i < total; i++ { + ID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + + sinkIDs := make([]string, 2) + for i := 0; i < 2; i++ { + sinkID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + sinkIDs = append(sinkIDs, sinkID.String()) + } + + validName, err := types.NewIdentifier(fmt.Sprintf("dataset-%d", i)) + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + + dataset := policies.Dataset{ + ID: ID.String(), + Name: validName, + PolicyID: policy.ID, + AgentGroupID: agentGroupID.String(), + SinkIDs: sinkIDs, + } + + ds, err := svc.AddDataset(context.Background(), token, dataset) + if err != nil { + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + } + + datasetsID[i] = ds.ID + } + oID, _ := identify(token, users) + + listPlTest := make([]policies.PolicyInDataset, total) + for i := 0; i < total; i++ { + listPlTest[i] = policies.PolicyInDataset{ + Policy: policy, + DatasetID: datasetsID[i], + } + } + + cases := map[string]struct { + ownerID string + groupID []string + policies []policies.PolicyInDataset + size uint64 + err error + }{ + "retrieve a list of policies by groupID": { + ownerID: oID, + groupID: []string{agentGroupID.String()}, + policies: listPlTest, + size: uint64(total), + err: nil, + }, + "retrieve a list of policies by non-existent groupID": { + ownerID: oID, + groupID: []string{oID}, + policies: []policies.PolicyInDataset{}, + size: uint64(0), + err: nil, + }, + "list with empty ownerID": { + ownerID: "", + groupID: []string{agentGroupID.String()}, + policies: nil, + size: 0, + err: policies.ErrMalformedEntity, + }, + } + + for desc, tc := range cases { + t.Run(desc, func(t *testing.T) { + policies, err := svc.ListPoliciesByGroupIDInternal(context.Background(), tc.groupID, tc.ownerID) + size := uint64(len(policies)) + assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d", desc, tc.size, size)) + assert.Equal(t, tc.policies, policies, fmt.Sprintf("%s: expected %p got %p", desc, tc.policies, policies)) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", desc, tc.err, err)) + }) + + } +} + +func TestRetrievePolicyByIDInternal(t *testing.T) { + users := flmocks.NewAuthService(map[string]string{token: email}) + svc := newService(users) + + policy := createPolicy(t, svc, "policy") + + oID, _ := identify(token, users) + + wrongPlID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + + cases := map[string]struct { + policyID string + ownerID string + err error + }{ + "view a existing policy": { + policyID: policy.ID, + ownerID: oID, + err: nil, + }, + "view policy with empty ownerID": { + policyID: policy.ID, + ownerID: "", + err: policies.ErrMalformedEntity, + }, + "view non-existing policy": { + policyID: wrongPlID.String(), + ownerID: oID, + err: policies.ErrNotFound, + }, + } + for desc, tc := range cases { + t.Run(desc, func(t *testing.T) { + _, err := svc.ViewPolicyByIDInternal(context.Background(), tc.policyID, tc.ownerID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", desc, tc.err, err)) + }) + } +} + +func TestListDatasetsByPolicyIDInternal(t *testing.T) { + users := flmocks.NewAuthService(map[string]string{token: email}) + svc := newService(users) + + wrongPlID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + + policy := createPolicy(t, svc, "policy") + + var total = 10 + + datasetsTest := make([]policies.Dataset, total) + + for i := 0; i < total; i++ { + ID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + + agentGroupID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + + sinkIDs := make([]string, 2) + for i := 0; i < 2; i++ { + sinkID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + sinkIDs = append(sinkIDs, sinkID.String()) + } + + validName, err := types.NewIdentifier(fmt.Sprintf("dataset-%d", i)) + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + + dataset := policies.Dataset{ + ID: ID.String(), + Name: validName, + PolicyID: policy.ID, + AgentGroupID: agentGroupID.String(), + SinkIDs: sinkIDs, + } + + ds, err := svc.AddDataset(context.Background(), token, dataset) + if err != nil { + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + } + + datasetsTest[i] = ds + } + + cases := map[string]struct { + token string + policyId string + size uint64 + err error + }{ + "retrieve a list of datasets by policyID": { + policyId: policy.ID, + token: token, + size: uint64(total), + err: nil, + }, + "retrieve a list of datasets by non-existent policyID": { + policyId: wrongPlID.String(), + token: token, + size: uint64(0), + err: nil, + }, + } + + for desc, tc := range cases { + t.Run(desc, func(t *testing.T) { + datasets, err := svc.ListDatasetsByPolicyIDInternal(context.Background(), tc.policyId, tc.token) + size := uint64(len(datasets)) + assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d", desc, tc.size, size)) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", desc, tc.err, err)) + }) + + } +} + +func TestRetrieveDatasetByIDInternal(t *testing.T) { + users := flmocks.NewAuthService(map[string]string{token: email}) + svc := newService(users) + + dataset := createDataset(t, svc, "dataset") + + oID, _ := identify(token, users) + + wrongPlID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + + cases := map[string]struct { + datasetID string + ownerID string + err error + }{ + "view a existing dataset": { + datasetID: dataset.ID, + ownerID: oID, + err: nil, + }, + "view non-existing policy": { + datasetID: wrongPlID.String(), + ownerID: oID, + err: policies.ErrNotFound, + }, + } + for desc, tc := range cases { + t.Run(desc, func(t *testing.T) { + _, err := svc.ViewDatasetByIDInternal(context.Background(), tc.ownerID, tc.datasetID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", desc, tc.err, err)) + }) + } +} + +func TestInactivateDatasetsByGroupID(t *testing.T) { + users := flmocks.NewAuthService(map[string]string{token: email}) + svc := newService(users) + + wrongGroupID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + + agentGroupID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + + policy := createPolicy(t, svc, "policy") + + var total = 10 + + datasetsTest := make([]policies.Dataset, total) + + for i := 0; i < total; i++ { + ID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + + sinkIDs := make([]string, 2) + for i := 0; i < 2; i++ { + sinkID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + sinkIDs = append(sinkIDs, sinkID.String()) + } + + validName, err := types.NewIdentifier(fmt.Sprintf("dataset-%d", i)) + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + + dataset := policies.Dataset{ + ID: ID.String(), + Name: validName, + PolicyID: policy.ID, + AgentGroupID: agentGroupID.String(), + SinkIDs: sinkIDs, + } + + ds, err := svc.AddDataset(context.Background(), token, dataset) + if err != nil { + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + } + + datasetsTest[i] = ds + } + + cases := map[string]struct { + token string + groupID string + err error + }{ + "inactivate a set of datasets by groupID": { + groupID: agentGroupID.String(), + token: token, + err: nil, + }, + "inactivate datasets with a non-existent groupID": { + groupID: wrongGroupID.String(), + token: token, + err: policies.ErrNotFound, + }, + "inactivate datasets with empty groupID": { + groupID: "", + token: token, + err: policies.ErrMalformedEntity, + }, + } + + for desc, tc := range cases { + t.Run(desc, func(t *testing.T) { + err := svc.InactivateDatasetByGroupID(context.Background(), tc.groupID, tc.token) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", desc, tc.err, err)) + }) + + } +} + func createPolicy(t *testing.T, svc policies.Service, name string) policies.Policy { t.Helper() ID, err := uuid.NewV4() @@ -816,3 +1140,15 @@ func testSortDataset(t *testing.T, pm policies.PageMetadata, ags []policies.Data break } } + +func identify(token string, auth mainflux.AuthServiceClient) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + res, err := auth.Identify(ctx, &mainflux.Token{Value: token}) + if err != nil { + return "", errors.Wrap(errors.ErrUnauthorizedAccess, err) + } + + return res.GetId(), nil +} \ No newline at end of file diff --git a/policies/postgres/datasets_test.go b/policies/postgres/datasets_test.go index 91c46c253..3e2497317 100644 --- a/policies/postgres/datasets_test.go +++ b/policies/postgres/datasets_test.go @@ -78,6 +78,17 @@ func TestDatasetSave(t *testing.T) { dataset: datasetCopy, err: errors.ErrConflict, }, + "create new dataset with empty ownerID": { + dataset: policies.Dataset{ + Name: nameID, + MFOwnerID: "", + Valid: true, + AgentGroupID: groupID.String(), + PolicyID: policyID.String(), + SinkIDs: sinkIDs, + }, + err: errors.ErrMalformedEntity, + }, } for desc, tc := range cases { @@ -101,6 +112,9 @@ func TestDatasetUpdate(t *testing.T) { policyID, err := uuid.NewV4() require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + wrongID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + sinkIDs := make([]string, 2) for i := 0; i < 2; i++ { sinkID, err := uuid.NewV4() @@ -135,6 +149,20 @@ func TestDatasetUpdate(t *testing.T) { dataset: dataset, err: nil, }, + "update a non-existing dataset": { + dataset: policies.Dataset{ + Name: nameID, + MFOwnerID: oID.String(), + Valid: true, + AgentGroupID: groupID.String(), + PolicyID: policyID.String(), + SinkIDs: sinkIDs, + Metadata: types.Metadata{"testkey": "testvalue"}, + Created: time.Time{}, + ID: wrongID.String(), + }, + err: policies.ErrNotFound, + }, } for desc, tc := range cases { @@ -152,6 +180,9 @@ func TestDatasetDelete(t *testing.T) { oID, err := uuid.NewV4() require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + wrongID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + groupID, err := uuid.NewV4() require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) @@ -194,6 +225,11 @@ func TestDatasetDelete(t *testing.T) { id: dataset.ID, err: nil, }, + "delete a non-existing dataset": { + owner: dataset.MFOwnerID, + id: wrongID.String(), + err: errors.ErrNotFound, + }, } for desc, tc := range cases { @@ -256,6 +292,16 @@ func TestDatasetRetrieveByID(t *testing.T) { ownerID: id, err: errors.ErrNotFound, }, + "retrieve dataset by ID and ownerID with emmpty owner field": { + datasetID: id, + ownerID: "", + err: errors.ErrMalformedEntity, + }, + "retrieve dataset by ID and ownerID with emmpty datasetID field": { + datasetID: "", + ownerID: dataset.MFOwnerID, + err: errors.ErrMalformedEntity, + }, } for desc, tc := range cases { @@ -374,6 +420,210 @@ func TestMultiDatasetRetrieval(t *testing.T) { } } +func TestInactivateDatasetByGroupID(t *testing.T) { + dbMiddleware := postgres.NewDatabase(db) + repo := postgres.NewPoliciesRepository(dbMiddleware, logger) + + oID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + wrongOID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + groupID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + wrongGroupID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + policyID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + sinkIDs := make([]string, 2) + for i := 0; i < 2; i++ { + sinkID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + sinkIDs[i] = sinkID.String() + } + + nameID, err := types.NewIdentifier("mydataset") + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + dataset := policies.Dataset{ + Name: nameID, + MFOwnerID: oID.String(), + Valid: true, + AgentGroupID: groupID.String(), + PolicyID: policyID.String(), + SinkIDs: sinkIDs, + Metadata: types.Metadata{"testkey": "testvalue"}, + Created: time.Time{}, + } + + dsID, err := repo.SaveDataset(context.Background(), dataset) + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + + dataset.ID = dsID + + cases := map[string]struct { + ownerID string + groupID string + err error + }{ + "inactivate a existing dataset by group ID": { + ownerID: dataset.MFOwnerID, + groupID: dataset.AgentGroupID, + err: nil, + }, + "inactivate a dataset with non-existent owner": { + groupID: dataset.AgentGroupID, + ownerID: wrongOID.String(), + err: policies.ErrInactivateDataset, + }, + "inactivate a non-existing dataset with existent owner": { + groupID: wrongGroupID.String(), + ownerID: dataset.MFOwnerID, + err: policies.ErrInactivateDataset, + }, + } + + for desc, tc := range cases { + t.Run(desc, func(t *testing.T) { + err := repo.InactivateDatasetByGroupID(context.Background(), tc.groupID, tc.ownerID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected '%s' got '%s'", desc, tc.err, err)) + }) + } +} + +func TestInactivateDatasetByPolicyID(t *testing.T) { + dbMiddleware := postgres.NewDatabase(db) + repo := postgres.NewPoliciesRepository(dbMiddleware, logger) + + oID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + groupID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + policyID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + sinkIDs := make([]string, 2) + for i := 0; i < 2; i++ { + sinkID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + sinkIDs[i] = sinkID.String() + } + + nameID, err := types.NewIdentifier("mydataset") + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + dataset := policies.Dataset{ + Name: nameID, + MFOwnerID: oID.String(), + Valid: true, + AgentGroupID: groupID.String(), + PolicyID: policyID.String(), + SinkIDs: sinkIDs, + Metadata: types.Metadata{"testkey": "testvalue"}, + Created: time.Time{}, + } + + dsID, err := repo.SaveDataset(context.Background(), dataset) + require.Nil(t, err, fmt.Sprintf("Unexpected error: %s", err)) + + dataset.ID = dsID + + cases := map[string]struct { + ownerID string + policyID string + err error + }{ + "inactivate a existing dataset by policy ID": { + ownerID: dataset.MFOwnerID, + policyID: dataset.PolicyID, + err: nil, + }, + } + + for desc, tc := range cases { + t.Run(desc, func(t *testing.T) { + err := repo.InactivateDatasetByPolicyID(context.Background(), tc.policyID, tc.ownerID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected '%s' got '%s'", desc, tc.err, err)) + }) + } +} + +func TestMultiDatasetRetrievalPolicyID(t *testing.T) { + dbMiddleware := postgres.NewDatabase(db) + repo := postgres.NewPoliciesRepository(dbMiddleware, logger) + + oID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + policyID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + wrongID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + n := uint64(10) + for i := uint64(0); i < n; i++ { + nameID, err := types.NewIdentifier(fmt.Sprintf("mydataset-%d", i)) + require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) + + dataset := policies.Dataset{ + Name: nameID, + MFOwnerID: oID.String(), + PolicyID: policyID.String(), + } + + _, err = repo.SaveDataset(context.Background(), dataset) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err)) + } + + cases := map[string]struct { + owner string + policyID string + size uint64 + err error + }{ + "retrieve all datasets by policyID": { + owner: oID.String(), + policyID: policyID.String(), + size: n, + err: nil, + }, + "retrieve datasets with no-existing owner": { + owner: wrongID.String(), + policyID: policyID.String(), + size: 0, + err: nil, + }, + "retrieve all datasets by policyID with empty policyID": { + owner: oID.String(), + policyID: "", + size: 0, + err: errors.ErrMalformedEntity, + }, + "retrieve datasets with no-existing policyID": { + owner: "", + policyID: policyID.String(), + size: 0, + err: errors.ErrMalformedEntity, + }, + } + + for desc, tc := range cases { + t.Run(desc, func(t *testing.T) { + datasets, err := repo.RetrieveDatasetsByPolicyID(context.Background(), tc.policyID, tc.owner) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected '%s' got '%s'", desc, tc.err, err)) + size := uint64(len(datasets)) + assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected size %d got %d", desc, tc.size, size)) + }) + } +} + func testSortDataset(t *testing.T, pm policies.PageMetadata, ags []policies.Dataset) { t.Helper() switch pm.Order { diff --git a/policies/postgres/init.go b/policies/postgres/init.go index 3d33b045a..b2f0a6ea3 100644 --- a/policies/postgres/init.go +++ b/policies/postgres/init.go @@ -89,6 +89,13 @@ func migrateDB(db *sqlx.DB) error { schema_version TEXT NOT NULL DEFAULT '1.0'`, }, }, + { + Id: "policies_3", + Up: []string{ + `ALTER TABLE IF EXISTS agent_policies ADD COLUMN IF NOT EXISTS + ts_last_modified TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL`, + }, + }, }, } diff --git a/policies/postgres/policies.go b/policies/postgres/policies.go index 3e0cfec81..73301f45e 100644 --- a/policies/postgres/policies.go +++ b/policies/postgres/policies.go @@ -52,7 +52,7 @@ func (r policiesRepository) DeletePolicy(ctx context.Context, ownerID string, po } func (r policiesRepository) UpdatePolicy(ctx context.Context, owner string, plcy policies.Policy) error { - q := `UPDATE agent_policies SET name = :name, description = :description, orb_tags = :orb_tags, policy = :policy, version = :version WHERE mf_owner_id = :mf_owner_id AND id = :id;` + q := `UPDATE agent_policies SET name = :name, description = :description, orb_tags = :orb_tags, policy = :policy, version = :version, ts_last_modified = CURRENT_TIMESTAMP WHERE mf_owner_id = :mf_owner_id AND id = :id;` plcyDB, err := toDBPolicy(plcy) if err != nil { return errors.Wrap(policies.ErrUpdateEntity, err) @@ -93,7 +93,7 @@ func (r policiesRepository) RetrieveAll(ctx context.Context, owner string, pm po return policies.Page{}, errors.Wrap(errors.ErrSelectEntity, err) } - q := fmt.Sprintf(`SELECT id, name, description, mf_owner_id, orb_tags, backend, version, policy, ts_created + q := fmt.Sprintf(`SELECT id, name, description, mf_owner_id, orb_tags, backend, version, policy, ts_created, ts_last_modified FROM agent_policies WHERE mf_owner_id = :mf_owner_id %s%s ORDER BY %s %s LIMIT :limit OFFSET :offset;`, nameQuery, tagsQuery, orderQuery, dirQuery) @@ -182,7 +182,8 @@ func (r policiesRepository) RetrievePoliciesByGroupID(ctx context.Context, group } func (r policiesRepository) RetrievePolicyByID(ctx context.Context, policyID string, ownerID string) (policies.Policy, error) { - q := `SELECT id, name, description, mf_owner_id, orb_tags, backend, version, policy, ts_created FROM agent_policies WHERE id = $1 AND mf_owner_id = $2` + q := `SELECT id, name, description, mf_owner_id, orb_tags, backend, version, policy, ts_created, ts_last_modified + FROM agent_policies WHERE id = $1 AND mf_owner_id = $2` if policyID == "" || ownerID == "" { return policies.Policy{}, errors.ErrMalformedEntity @@ -515,6 +516,7 @@ type dbPolicy struct { Version int32 `db:"version"` Created time.Time `db:"ts_created"` DataSetID string `db:"dataset_id"` + LastModified time.Time `db:"ts_last_modified"` } func toDBPolicy(policy policies.Policy) (dbPolicy, error) { @@ -605,6 +607,7 @@ func toPolicy(dba dbPolicy) policies.Policy { OrbTags: types.Tags(dba.OrbTags), Policy: types.Metadata(dba.Policy), Created: dba.Created, + LastModified: dba.LastModified, } return policy diff --git a/policies/postgres/policies_test.go b/policies/postgres/policies_test.go index c0097f0a4..cd14b0643 100644 --- a/policies/postgres/policies_test.go +++ b/policies/postgres/policies_test.go @@ -65,6 +65,13 @@ func TestPolicySave(t *testing.T) { policy: policyCopy, err: errors.ErrConflict, }, + "create new policy with empty owner": { + policy: policies.Policy{ + Name: nameID, + MFOwnerID: "", + }, + err: errors.ErrMalformedEntity, + }, } for desc, tc := range cases { @@ -111,6 +118,18 @@ func TestAgentPolicyDataRetrieve(t *testing.T) { policyID: policy.MFOwnerID, err: errors.ErrNotFound, }, + "retrieve policies by ID with empty owner": { + policyID: id, + ownerID: "", + tags: types.Tags{"testkey": "testvalue"}, + err: errors.ErrMalformedEntity, + }, + "retrieve policies by ID with empty policyID": { + policyID: "", + ownerID: policy.MFOwnerID, + tags: types.Tags{"testkey": "testvalue"}, + err: errors.ErrMalformedEntity, + }, } for desc, tc := range cases { @@ -309,6 +328,20 @@ func TestAgentPoliciesRetrieveByGroup(t *testing.T) { results: 0, err: nil, }, + "retrieve policies by groupID with empty owner": { + groupID: []string{policy.MFOwnerID}, + ownerID: "", + dsID: dsID, + results: 0, + err: errors.ErrMalformedEntity, + }, + "retrieve policies by groupID with empty groupID": { + groupID: []string{}, + ownerID: policy.MFOwnerID, + dsID: dsID, + results: 0, + err: errors.ErrMalformedEntity, + }, } for desc, tc := range cases { diff --git a/python-test/.gitignore b/python-test/.gitignore index 483895b15..7ff6a38dd 100644 --- a/python-test/.gitignore +++ b/python-test/.gitignore @@ -1,3 +1,5 @@ test_config.ini reports/ -behave_test/ \ No newline at end of file +behave_test/ +/site +/env_doc \ No newline at end of file diff --git a/python-test/docs/agent_groups/check_agent_groups_details.md b/python-test/docs/agent_groups/check_agent_groups_details.md new file mode 100644 index 000000000..b546a51a7 --- /dev/null +++ b/python-test/docs/agent_groups/check_agent_groups_details.md @@ -0,0 +1,17 @@ +## Scenario: Check agent groups details +## Steps: +1 - Create an agent group + +- REST API Method: POST +- endpoint: /agent_groups +- header: {authorization:token} + +2 - Get an agent group + +- REST API Method: GET +- endpoint: /agent_groups/agent_group_id + +## Expected Result: +- Status code must be 200 and the group name, description, matches against and tags must be returned on response + + diff --git a/python-test/docs/agent_groups/check_if_is_possible_cancel_operations_with_no_change.md b/python-test/docs/agent_groups/check_if_is_possible_cancel_operations_with_no_change.md new file mode 100644 index 000000000..6b849fe37 --- /dev/null +++ b/python-test/docs/agent_groups/check_if_is_possible_cancel_operations_with_no_change.md @@ -0,0 +1,17 @@ +## Scenario: Check if is possible cancel operations with no change +## Steps: +1 - Create an agent group + +- REST API Method: POST +- endpoint: /agent_groups +- header: {authorization:token} + +2 - On agent groups' page (`orb.live/pages/fleet/groups`) click on edit button +3 - Change groups' name and click "next" +4 - Change groups' description and click "next" +4 - Change groups' tag and click "next" +5 - Click "back" until return to agent groups' page + +## Expected Result: +- No changes must have been applied to the agent group + diff --git a/python-test/docs/agent_groups/check_if_total_agent_groups_on_agent_groups'_page_is_correct.md b/python-test/docs/agent_groups/check_if_total_agent_groups_on_agent_groups'_page_is_correct.md new file mode 100644 index 000000000..f9d308d47 --- /dev/null +++ b/python-test/docs/agent_groups/check_if_total_agent_groups_on_agent_groups'_page_is_correct.md @@ -0,0 +1,20 @@ +## Scenario: Check if total agent groups on agent groups' page is correct +## Steps: +1 - Create multiple agent groups + +- REST API Method: POST +- endpoint: /agent_groups +- header: {authorization:token} + +2 - Get all existing agent groups + +- REST API Method: GET +- endpoint: /agent_groups + +3 - On agent groups' page (`orb.live/pages/fleet/groups`) check the total number of agent groups at the end of the agent groups table + +4 - Count the number of existing agent groups + +## Expected Result: +- Total agent groups on API response, agent groups page and the real number must be the same + diff --git a/python-test/docs/agent_groups/create_agent_group_with_description.md b/python-test/docs/agent_groups/create_agent_group_with_description.md new file mode 100644 index 000000000..1cb2dd29c --- /dev/null +++ b/python-test/docs/agent_groups/create_agent_group_with_description.md @@ -0,0 +1,12 @@ +## Scenario: Create agent group with description +## Steps: + +1 - Create an agent groups with description + +- REST API Method: POST +- endpoint: /agent_groups +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 201 (created) and the agent group must be created diff --git a/python-test/docs/agent_groups/create_agent_group_with_duplicate_name.md b/python-test/docs/agent_groups/create_agent_group_with_duplicate_name.md new file mode 100644 index 000000000..5ecdbb2e9 --- /dev/null +++ b/python-test/docs/agent_groups/create_agent_group_with_duplicate_name.md @@ -0,0 +1,13 @@ +## Scenario: Create agent group with duplicate name +## Steps: +1 - Create an agent group + +- REST API Method: POST +- endpoint: /agent_groups +- header: {authorization:token} + +2 - Create another agent group using the same agent group name + +## Expected Result: +- First request must have status code 201 (created) and one group must be created on orb +- Second request must fail with status code 409 (conflict) and no other group must be created (make sure that first group has not been modified) diff --git a/python-test/docs/agent_groups/create_agent_group_with_invalid_name_(regex).md b/python-test/docs/agent_groups/create_agent_group_with_invalid_name_(regex).md new file mode 100644 index 000000000..3febc8735 --- /dev/null +++ b/python-test/docs/agent_groups/create_agent_group_with_invalid_name_(regex).md @@ -0,0 +1,15 @@ +## Scenario: Create agent group with invalid name (regex) +## Steps: +1 - Create an agent group using an invalid regex to agent group name + +- REST API Method: POST +- endpoint: /agent_groups +- header: {authorization:token} +- example of invalid regex: + + * name starting with non-alphabetic characters + * name with just 1 letter + * space-separated composite name + +## Expected Result: +- Request must fail with status code 400 (bad request) and no group must be created diff --git a/python-test/docs/agent_groups/create_agent_group_with_multiple_tags.md b/python-test/docs/agent_groups/create_agent_group_with_multiple_tags.md new file mode 100644 index 000000000..19ebb3790 --- /dev/null +++ b/python-test/docs/agent_groups/create_agent_group_with_multiple_tags.md @@ -0,0 +1,12 @@ +## Scenario: Create agent group with multiple tags +## Steps: +1 - Create an agent group with more than one pair (key:value) of tags + +- REST API Method: POST +- endpoint: /agent_groups +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 201 (created) and the agent group must be created +- Groups with multiple tags will only match with agents with the same multiple tags \ No newline at end of file diff --git a/python-test/docs/agent_groups/create_agent_group_with_one_tag.md b/python-test/docs/agent_groups/create_agent_group_with_one_tag.md new file mode 100644 index 000000000..969bb19b5 --- /dev/null +++ b/python-test/docs/agent_groups/create_agent_group_with_one_tag.md @@ -0,0 +1,12 @@ +## Scenario: Create agent group with one tag +## Steps: + +1 - Create an agent groups with one pair (key:value) of tags + +- REST API Method: POST +- endpoint: /agent_groups +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 201 (created) and the agent group must be created \ No newline at end of file diff --git a/python-test/docs/agent_groups/create_agent_group_without_description.md b/python-test/docs/agent_groups/create_agent_group_without_description.md new file mode 100644 index 000000000..ddfd9f40c --- /dev/null +++ b/python-test/docs/agent_groups/create_agent_group_without_description.md @@ -0,0 +1,12 @@ +## Scenario: Create agent group without description +## Steps: + +1 - Create an agent groups with no description + +- REST API Method: POST +- endpoint: /agent_groups +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 201 (created) and the agent group must be created diff --git a/python-test/docs/agent_groups/create_agent_group_without_tag.md b/python-test/docs/agent_groups/create_agent_group_without_tag.md new file mode 100644 index 000000000..1b9ded0ab --- /dev/null +++ b/python-test/docs/agent_groups/create_agent_group_without_tag.md @@ -0,0 +1,13 @@ +## Scenario: Create agent group without tag + +## Steps: +1 - Create an agent with no pair (key:value) of tags + +- REST API Method: POST +- endpoint: /agent_groups +- header: {authorization:token} + + +## Expected Result: +- Request must fail with status code 400 (bad request) and the agent group must not be created + diff --git a/python-test/docs/agent_groups/edit_agent_group_description.md b/python-test/docs/agent_groups/edit_agent_group_description.md new file mode 100644 index 000000000..244d76c67 --- /dev/null +++ b/python-test/docs/agent_groups/edit_agent_group_description.md @@ -0,0 +1,17 @@ +## Scenario: Edit agent group description +## Steps: +1 - Create an agent group + +- REST API Method: POST +- endpoint: /agent_groups +- header: {authorization:token} + +2- Edit this group description + +- REST API Method: PUT +- endpoint: /agent_groups/group_id +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 200 (ok) and changes must be applied diff --git a/python-test/docs/agent_groups/edit_agent_group_name.md b/python-test/docs/agent_groups/edit_agent_group_name.md new file mode 100644 index 000000000..e5b1bbed7 --- /dev/null +++ b/python-test/docs/agent_groups/edit_agent_group_name.md @@ -0,0 +1,17 @@ +## Scenario: Edit agent group name +## Steps: +1 - Create an agent group + +- REST API Method: POST +- endpoint: /agent_groups +- header: {authorization:token} + +2- Edit this group name + +- REST API Method: PUT +- endpoint: /agent_groups/group_id +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 200 (ok) and changes must be applied diff --git a/python-test/docs/agent_groups/edit_agent_group_tag.md b/python-test/docs/agent_groups/edit_agent_group_tag.md new file mode 100644 index 000000000..5ccb0c71b --- /dev/null +++ b/python-test/docs/agent_groups/edit_agent_group_tag.md @@ -0,0 +1,18 @@ +## Scenario: Edit agent group tag +## Steps: +1 - Create an agent group + +- REST API Method: POST +- endpoint: /agent_groups +- header: {authorization:token} + +2- Edit this group tag + +- REST API Method: PUT +- endpoint: /agent_groups/group_id +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 200 (ok) and changes must be applied + diff --git a/python-test/docs/agent_groups/edit_an_agent_group_through_the_details_modal.md b/python-test/docs/agent_groups/edit_an_agent_group_through_the_details_modal.md new file mode 100644 index 000000000..f416d8788 --- /dev/null +++ b/python-test/docs/agent_groups/edit_an_agent_group_through_the_details_modal.md @@ -0,0 +1,13 @@ +## Scenario: Edit an agent group through the details modal +## Steps: +1 - Create an agent group + +- REST API Method: POST +- endpoint: /agent_groups +- header: {authorization:token} + +2 - On agent groups' page (`orb.live/pages/fleet/groups`) click on details button +3 - Click on "edit" button + +## Expected Result: +- User should be redirected to this agent group's edit page and should be able to make changes diff --git a/python-test/docs/agent_groups/remove_agent_group_using_correct_name.md b/python-test/docs/agent_groups/remove_agent_group_using_correct_name.md new file mode 100644 index 000000000..7e84779b5 --- /dev/null +++ b/python-test/docs/agent_groups/remove_agent_group_using_correct_name.md @@ -0,0 +1,16 @@ +## Scenario: Remove agent group using correct name +## Steps: +1 - Create an agent group + +- REST API Method: POST +- endpoint: /agent_groups +- header: {authorization:token} + +2 - On agent groups' page (`orb.live/pages/fleet/groups`) click on remove button +3 - Insert the name of the group correctly on delete modal +4 - Confirm the operation by clicking on "I UNDERSTAND, DELETE THIS AGENT GROUP" button + +## Expected Result: +- Agent group must be deleted + + diff --git a/python-test/docs/agent_groups/remove_agent_group_using_incorrect_name.md b/python-test/docs/agent_groups/remove_agent_group_using_incorrect_name.md new file mode 100644 index 000000000..5d50d81e6 --- /dev/null +++ b/python-test/docs/agent_groups/remove_agent_group_using_incorrect_name.md @@ -0,0 +1,14 @@ +## Scenario: Remove agent group using incorrect name +## Steps: +1 - Create an agent group + +- REST API Method: POST +- endpoint: /agent_groups +- header: {authorization:token} + +2 - On agent groups' page (`orb.live/pages/fleet/groups`) click on remove button +3 - Insert the name of the group incorrectly on delete modal + +## Expected Result: +- "I UNDERSTAND, DELETE THIS AGENT GROUP" button must not be enabled +- After user close the deletion modal, agent group must not be deleted diff --git a/python-test/docs/agent_groups/test_agent_groups_filters.md b/python-test/docs/agent_groups/test_agent_groups_filters.md new file mode 100644 index 000000000..a30335fb2 --- /dev/null +++ b/python-test/docs/agent_groups/test_agent_groups_filters.md @@ -0,0 +1,22 @@ +## Scenario: Test agent groups filters +## Steps: +1 - Create multiple agent groups + +- REST API Method: POST +- endpoint: /agent_groups +- header: {authorization:token} + +2 - On agent groups' page (`orb.live/pages/fleet/groups`) use the filter: + +* Name +* Description +* Agents +* Tags +* Search by + + +## Expected Result: + +- All filters must be working properly + + diff --git a/python-test/docs/agent_groups/visualize_matching_agents.md b/python-test/docs/agent_groups/visualize_matching_agents.md new file mode 100644 index 000000000..264d60c4a --- /dev/null +++ b/python-test/docs/agent_groups/visualize_matching_agents.md @@ -0,0 +1,10 @@ +## Scenario: Visualize matching agents +## Steps: +1 - On agent groups' page (`orb.live/pages/fleet/groups`) click on the number with link o "agents" column + +## Expected Result: + +- Matching Agents modal must be displayed + + - If 0 agents matches: `No data to display` and 0 total + - If one or more agents matches: all matching agents and total number of matches ust be displayed \ No newline at end of file diff --git a/python-test/docs/agents/check_agent_details.md b/python-test/docs/agents/check_agent_details.md new file mode 100644 index 000000000..ae080946e --- /dev/null +++ b/python-test/docs/agents/check_agent_details.md @@ -0,0 +1,18 @@ +## Scenario: Check agent details +## Steps: +1 - Create an agent + +- REST API Method: POST +- endpoint: /agents +- header: {authorization:token} + +2 - Get an agent + +- REST API Method: GET +- endpoint: /agents/agent_id + +## Expected Result: +- Status code must be 200 and an agent name, channel id, ts_created, status and tags must be returned on response + * If an agent container was never provisioned, status must be `new` + * If an agent container is running, status must be `online` + * If an agent container is stopped/removed, status must be `offline` diff --git a/python-test/docs/agents/check_if_is_possible_cancel_operations_with_no_change.md b/python-test/docs/agents/check_if_is_possible_cancel_operations_with_no_change.md new file mode 100644 index 000000000..df32ebb1c --- /dev/null +++ b/python-test/docs/agents/check_if_is_possible_cancel_operations_with_no_change.md @@ -0,0 +1,16 @@ +## Scenario: Check if is possible cancel operations with no change +## Steps: +1 - Create an agent + +- REST API Method: POST +- endpoint: /agents +- header: {authorization:token} + +2 - On agents' page (`orb.live/pages/fleet/agents`) click on edit button +3 - Change agents' name and click "next" +4 - Change agent's tag and click "next" +5 - Click "back" until return to agents' page + +## Expected Result: +- No changes must have been applied to the agent + diff --git a/python-test/docs/agents/check_if_total_agent_on_agents'_page_is_correct.md b/python-test/docs/agents/check_if_total_agent_on_agents'_page_is_correct.md new file mode 100644 index 000000000..7cec046f2 --- /dev/null +++ b/python-test/docs/agents/check_if_total_agent_on_agents'_page_is_correct.md @@ -0,0 +1,19 @@ +## Scenario: Check if total agent on agents' page is correct +## Steps: +1 - Create multiple agents + +- REST API Method: POST +- endpoint: /agents +- header: {authorization:token} + +2 - Get all existing agents + +- REST API Method: GET +- endpoint: /agents + +3 - On agents' page (`orb.live/pages/fleet/agents`) check the total number of agents at the end of the agents table + +4 - Count the number of existing agents + +## Expected Result: +- Total agents on API response, agents page and the real number must be the same diff --git a/python-test/docs/agents/create_agent_with_duplicate_name.md b/python-test/docs/agents/create_agent_with_duplicate_name.md new file mode 100644 index 000000000..d4ab6e30e --- /dev/null +++ b/python-test/docs/agents/create_agent_with_duplicate_name.md @@ -0,0 +1,13 @@ +## Scenario: Create agent with duplicate name +## Steps: +1 - Create an agent + +- REST API Method: POST +- endpoint: /agents +- header: {authorization:token} + +2 - Create another agent using the same agent name + +## Expected Result: +- First request must have status code 201 (created) and one agent must be created on orb +- Second request must fail with status code 409 (conflict) and no other agent must be created (make sure that first agent has not been modified) diff --git a/python-test/docs/agents/create_agent_with_invalid_name_(regex).md b/python-test/docs/agents/create_agent_with_invalid_name_(regex).md new file mode 100644 index 000000000..46c6647b5 --- /dev/null +++ b/python-test/docs/agents/create_agent_with_invalid_name_(regex).md @@ -0,0 +1,15 @@ +## Scenario: Create agent with invalid name (regex) +## Steps: +1 - Create an agent using an invalid regex to agent name + +- REST API Method: POST +- endpoint: /agents +- header: {authorization:token} +- example of invalid regex: + + * name starting with non-alphabetic characters + * name with just 1 letter + * space-separated composite name + +## Expected Result: +- Request must fail with status code 400 (bad request) and no agent must be created diff --git a/python-test/docs/agents/create_agent_with_multiple_tags.md b/python-test/docs/agents/create_agent_with_multiple_tags.md new file mode 100644 index 000000000..e635a4b4d --- /dev/null +++ b/python-test/docs/agents/create_agent_with_multiple_tags.md @@ -0,0 +1,12 @@ +## Scenario: Create agent with multiple tags +## Steps: +1 - Create an agent with more than one pair (key:value) of tags + +- REST API Method: POST +- endpoint: /agents +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 201 (created) and the agent must be created +- Agent with multiple tags will match each tag individually diff --git a/python-test/docs/agents/create_agent_with_one_tag.md b/python-test/docs/agents/create_agent_with_one_tag.md new file mode 100644 index 000000000..1d4c52c3b --- /dev/null +++ b/python-test/docs/agents/create_agent_with_one_tag.md @@ -0,0 +1,12 @@ +## Scenario: Create agent with one tag +## Steps: + +1 - Create an agent with one pair (key:value) of tags + +- REST API Method: POST +- endpoint: /agents +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 201 (created) and the agent must be created diff --git a/python-test/docs/agents/create_agent_without_tags.md b/python-test/docs/agents/create_agent_without_tags.md new file mode 100644 index 000000000..fb43439fb --- /dev/null +++ b/python-test/docs/agents/create_agent_without_tags.md @@ -0,0 +1,13 @@ +## Scenario: Create agent without tags + +## Steps: +1 - Create an agent with no pair (key:value) of tags + +- REST API Method: POST +- endpoint: /agents +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 201 (created) and the agent must be created + diff --git a/python-test/docs/agents/edit_agent_name.md b/python-test/docs/agents/edit_agent_name.md new file mode 100644 index 000000000..8fb911f02 --- /dev/null +++ b/python-test/docs/agents/edit_agent_name.md @@ -0,0 +1,18 @@ +## Scenario: Edit agent name + +## Steps: +1 - Create an agent + +- REST API Method: POST +- endpoint: /agents +- header: {authorization:token} + +2- Edit this agent name + +- REST API Method: PUT +- endpoint: /agents/agent_id +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 200 (ok) and changes must be applied diff --git a/python-test/docs/agents/edit_agent_name_and_tags.md b/python-test/docs/agents/edit_agent_name_and_tags.md new file mode 100644 index 000000000..fe1b29a4a --- /dev/null +++ b/python-test/docs/agents/edit_agent_name_and_tags.md @@ -0,0 +1,17 @@ +## Scenario: Edit agent name and tags +## Steps: +1 - Create an agent + +- REST API Method: POST +- endpoint: /agents +- header: {authorization:token} + +2- Edit this agent and tags + +- REST API Method: PUT +- endpoint: /agents/agent_id +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 200 (ok) and changes must be applied \ No newline at end of file diff --git a/python-test/docs/agents/edit_agent_tag.md b/python-test/docs/agents/edit_agent_tag.md new file mode 100644 index 000000000..5d5a0049b --- /dev/null +++ b/python-test/docs/agents/edit_agent_tag.md @@ -0,0 +1,19 @@ +## Scenario: Edit agent tag + +## Steps: +1 - Create an agent + +- REST API Method: POST +- endpoint: /agents +- header: {authorization:token} + +2- Edit this agent tag + +- REST API Method: PUT +- endpoint: /agents/agent_id +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 200 (ok) and changes must be applied + diff --git a/python-test/docs/agents/edit_an_agent_through_the_details_modal.md b/python-test/docs/agents/edit_an_agent_through_the_details_modal.md new file mode 100644 index 000000000..71bf39677 --- /dev/null +++ b/python-test/docs/agents/edit_an_agent_through_the_details_modal.md @@ -0,0 +1,14 @@ +## Scenario: Edit an agent through the details modal +## Steps: +1 - Create an agent + +- REST API Method: POST +- endpoint: /agents +- header: {authorization:token} + +2 - On agents' page (`orb.live/pages/fleet/agents`) click on details button +3 - Click on "edit" button + +## Expected Result: +- User should be redirected to this agent's edit page and should be able to make changes + diff --git a/python-test/docs/agents/insert_tags_in_agents_created_without_tags.md b/python-test/docs/agents/insert_tags_in_agents_created_without_tags.md new file mode 100644 index 000000000..880755759 --- /dev/null +++ b/python-test/docs/agents/insert_tags_in_agents_created_without_tags.md @@ -0,0 +1,17 @@ +## Scenario: Insert tags in agents created without tags +## Steps: +1 - Create an agent with no pair (key:value) of tags + +- REST API Method: POST +- endpoint: /agents +- header: {authorization:token} + +2- Edit this agent and insert at least one pair of tag + +- REST API Method: PUT +- endpoint: /agents/agent_id +- header: {authorization:token} + +## Expected Result: + +- Request must have status code 200 and tags must be added to the agent \ No newline at end of file diff --git a/python-test/docs/agents/remove_agent_using_correct_name.md b/python-test/docs/agents/remove_agent_using_correct_name.md new file mode 100644 index 000000000..7e9c8b94c --- /dev/null +++ b/python-test/docs/agents/remove_agent_using_correct_name.md @@ -0,0 +1,14 @@ +## Scenario: Remove agent using correct name +## Steps: +1 - Create an agent + +- REST API Method: POST +- endpoint: /agents +- header: {authorization:token} + +2 - On agents' page (`orb.live/pages/fleet/agents`) click on remove button +3 - Insert the name of the agent correctly on delete modal +4 - Confirm the operation by clicking on "I UNDERSTAND, DELETE THIS AGENT" button + +## Expected Result: +- Agent must be deleted diff --git a/python-test/docs/agents/remove_agent_using_incorrect_name.md b/python-test/docs/agents/remove_agent_using_incorrect_name.md new file mode 100644 index 000000000..33b78fb1f --- /dev/null +++ b/python-test/docs/agents/remove_agent_using_incorrect_name.md @@ -0,0 +1,15 @@ +## Scenario: Remove agent using incorrect name +## Steps: +1 - Create an agent + +- REST API Method: POST +- endpoint: /agents +- header: {authorization:token} + +2 - On agents' page (`orb.live/pages/fleet/agents`) click on remove button +3 - Insert the name of the agent incorrectly on delete modal + +## Expected Result: +- "I UNDERSTAND, DELETE THIS AGENT" button must not be enabled +- After user close the deletion modal, agent must not be deleted + diff --git a/python-test/docs/agents/run_two_orb_agents_on_different_ports.md b/python-test/docs/agents/run_two_orb_agents_on_different_ports.md new file mode 100644 index 000000000..2e72cd28b --- /dev/null +++ b/python-test/docs/agents/run_two_orb_agents_on_different_ports.md @@ -0,0 +1,8 @@ +## Scenario: Run two orb agents on different ports + +## Steps: +1 - Provision an agent +2 - Provision another agent on a different port + - Use environmental variable: `PKTVISOR_PCAP_IFACE_DEFAULT` to set the port +## Expected Result: +- Both containers must be running \ No newline at end of file diff --git a/python-test/docs/agents/run_two_orb_agents_on_the_same_port.md b/python-test/docs/agents/run_two_orb_agents_on_the_same_port.md new file mode 100644 index 000000000..3bfb1c7bb --- /dev/null +++ b/python-test/docs/agents/run_two_orb_agents_on_the_same_port.md @@ -0,0 +1,9 @@ +## Scenario: Run two orb agents on the same port + +## Steps: +1 - Provision an agent +2 - Provision another agent on same port + +## Expected Result: +- Second container must be exited +- the container logs should contain the message "agent startup error" \ No newline at end of file diff --git a/python-test/docs/agents/save_agent_without_tag.md b/python-test/docs/agents/save_agent_without_tag.md new file mode 100644 index 000000000..7f7fda807 --- /dev/null +++ b/python-test/docs/agents/save_agent_without_tag.md @@ -0,0 +1,16 @@ +## Scenario: Save agent without tag +## Steps: +1 - Create an agent with at least one pair (key:value) of tags + +- REST API Method: POST +- endpoint: /agents +- header: {authorization:token} + +2- Edit this agent tag and remove all pairs + +- REST API Method: PUT +- endpoint: /agents/agent_id +- header: {authorization:token} + +## Expected Result: +- Request must have status code 200 and all tags must be removed from the agent diff --git a/python-test/docs/agents/test_agent_filters.md b/python-test/docs/agents/test_agent_filters.md new file mode 100644 index 000000000..4e6ea363e --- /dev/null +++ b/python-test/docs/agents/test_agent_filters.md @@ -0,0 +1,19 @@ +## Scenario: Test agent filters +## Steps: +1 - Create multiple agents + +- REST API Method: POST +- endpoint: /agents +- header: {authorization:token} + +2 - On agents' page (`orb.live/pages/fleet/agents`) use the filter: + + * Name + * Status + * Tags + * Search by + + +## Expected Result: + +- All filters must be working properly diff --git a/python-test/docs/datasets/check_datasets_details.md b/python-test/docs/datasets/check_datasets_details.md new file mode 100644 index 000000000..947d2d7ed --- /dev/null +++ b/python-test/docs/datasets/check_datasets_details.md @@ -0,0 +1,17 @@ +## Scenario: Check datasets details +## Steps: +1 - Create a dataset + +- REST API Method: POST +- endpoint: /policies/dataset +- header: {authorization:token} + +2 - Get a dataset + +- REST API Method: GET +- endpoint: /policies/dataset/dataset_id + +## Expected Result: +- Status code must be 200 and the dataset name, validity, agent group linked, agent policy linked and sink linked must be returned on response + + \ No newline at end of file diff --git a/python-test/docs/datasets/check_if_is_possible_cancel_operations_with_no_change.md b/python-test/docs/datasets/check_if_is_possible_cancel_operations_with_no_change.md new file mode 100644 index 000000000..1b15b2637 --- /dev/null +++ b/python-test/docs/datasets/check_if_is_possible_cancel_operations_with_no_change.md @@ -0,0 +1,14 @@ +## Scenario: Check if is possible cancel operations with no change +## Steps: +1 - Create a dataset + +- REST API Method: POST +- endpoint: /policies/dataset +- header: {authorization:token} + +2 - On datasets' page (`orb.live/pages/datasets/list`) click on edit button +3 - Change groups' name and click "next" +4 - Change sink linked and click "next" + +## Expected Result: +- No changes must have been applied to the dataset \ No newline at end of file diff --git a/python-test/docs/datasets/check_if_total_datasets_on_datasets'_page_is_correct.md b/python-test/docs/datasets/check_if_total_datasets_on_datasets'_page_is_correct.md new file mode 100644 index 000000000..c34b5d9e6 --- /dev/null +++ b/python-test/docs/datasets/check_if_total_datasets_on_datasets'_page_is_correct.md @@ -0,0 +1,20 @@ +## Scenario: Check if total datasets on datasets' page is correct +## Steps: +1 - Create multiple datasets + +- REST API Method: POST +- endpoint: /policies/dataset +- header: {authorization:token} + +2 - Get all existing datasets + +- REST API Method: GET +- endpoint: /policies/dataset + +3 - On datasets' page (`orb.live/pages/datasets/list`) check the total number of datasets at the end of the dataset table + +4 - Count the number of existing datasets + +## Expected Result: +- Total datasets on API response, datasets page and the real number must be the same + diff --git a/python-test/docs/datasets/create_dataset.md b/python-test/docs/datasets/create_dataset.md new file mode 100644 index 000000000..437e4acef --- /dev/null +++ b/python-test/docs/datasets/create_dataset.md @@ -0,0 +1,12 @@ +## Scenario: Create dataset +## Steps: + +1 - Create a dataset with no description + +- REST API Method: POST +- endpoint: /policies/dataset +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 201 (created) and the agent group must be created \ No newline at end of file diff --git a/python-test/docs/datasets/create_dataset_with_invalid_name_(regex).md b/python-test/docs/datasets/create_dataset_with_invalid_name_(regex).md new file mode 100644 index 000000000..0c1cd853e --- /dev/null +++ b/python-test/docs/datasets/create_dataset_with_invalid_name_(regex).md @@ -0,0 +1,15 @@ +## Scenario: Create dataset with invalid name (regex) +## Steps: +1 - Create a dataset using an invalid regex to dataset name + +- REST API Method: POST +- endpoint: /policies/dataset +- header: {authorization:token} +- example of invalid regex: + +* name starting with non-alphabetic characters +* name with just 1 letter +* space-separated composite name + +## Expected Result: +- Request must fail with status code 400 (bad request) and no dataset must be created \ No newline at end of file diff --git a/python-test/docs/datasets/edit_a_dataset_through_the_details_modal.md b/python-test/docs/datasets/edit_a_dataset_through_the_details_modal.md new file mode 100644 index 000000000..c89db9d65 --- /dev/null +++ b/python-test/docs/datasets/edit_a_dataset_through_the_details_modal.md @@ -0,0 +1,13 @@ +## Scenario: Edit a dataset through the details modal +## Steps: +1 - Create a dataset + +- REST API Method: POST +- endpoint: /policies/dataset +- header: {authorization:token} + +2 - On datasets' page (`orb.live/pages/datasets/list`) click on details button +3 - Click on "edit" button + +## Expected Result: +- User should be redirected to this dataset's edit page and should be able to make changes \ No newline at end of file diff --git a/python-test/docs/datasets/edit_dataset_name.md b/python-test/docs/datasets/edit_dataset_name.md new file mode 100644 index 000000000..33032682c --- /dev/null +++ b/python-test/docs/datasets/edit_dataset_name.md @@ -0,0 +1,17 @@ +## Scenario: Edit dataset name +## Steps: +1 - Create a dataset + +- REST API Method: POST +- endpoint: /policies/dataset +- header: {authorization:token} + +2- Edit this dataset name + +- REST API Method: PUT +- endpoint: /policies/dataset/dataset_id +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 200 (ok) and changes must be applied \ No newline at end of file diff --git a/python-test/docs/datasets/edit_dataset_sink.md b/python-test/docs/datasets/edit_dataset_sink.md new file mode 100644 index 000000000..67701ff1c --- /dev/null +++ b/python-test/docs/datasets/edit_dataset_sink.md @@ -0,0 +1,17 @@ +## Scenario: Edit dataset sink +## Steps: +1 - Create a dataset + +- REST API Method: POST +- endpoint: /policies/dataset +- header: {authorization:token} + +2- Edit this dataset sink + +- REST API Method: PUT +- endpoint: /policies/dataset/dataset_id +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 200 (ok) and changes must be applied \ No newline at end of file diff --git a/python-test/docs/datasets/remove_dataset_using_correct_name.md b/python-test/docs/datasets/remove_dataset_using_correct_name.md new file mode 100644 index 000000000..8687b0096 --- /dev/null +++ b/python-test/docs/datasets/remove_dataset_using_correct_name.md @@ -0,0 +1,14 @@ +## Scenario: Remove dataset using correct name +## Steps: +1 - Create a dataset + +- REST API Method: POST +- endpoint: /policies/dataset +- header: {authorization:token} + +2 - On datasets' page (`orb.live/pages/datasets/list`) click on remove button +3 - Insert the name of the dataset correctly on delete modal +4 - Confirm the operation by clicking on "I UNDERSTAND, DELETE THIS DATASET" button + +## Expected Result: +- Dataset must be deleted diff --git a/python-test/docs/datasets/remove_dataset_using_incorrect_name.md b/python-test/docs/datasets/remove_dataset_using_incorrect_name.md new file mode 100644 index 000000000..45fce22b4 --- /dev/null +++ b/python-test/docs/datasets/remove_dataset_using_incorrect_name.md @@ -0,0 +1,14 @@ +## Scenario: Remove dataset using incorrect name +## Steps: +1 - Create a dataset + +- REST API Method: POST +- endpoint: /policies/dataset +- header: {authorization:token} + +2 - On datasets' page (`orb.live/pages/datasets/list`) click on remove button +3 - Insert the name of the dataset correctly on delete modal + +## Expected Result: +- "I UNDERSTAND, DELETE THIS DATASET" button must not be enabled +- After user close the deletion modal, dataset must not be deleted diff --git a/python-test/docs/datasets/test_datasets_filter.md b/python-test/docs/datasets/test_datasets_filter.md new file mode 100644 index 000000000..d4ab2af87 --- /dev/null +++ b/python-test/docs/datasets/test_datasets_filter.md @@ -0,0 +1,17 @@ +## Scenario: Test datasets filter +## Steps: +1 - Create multiple datasets + +- REST API Method: POST +- endpoint: /policies/dataset +- header: {authorization:token} + +2 - On datasets' page (`orb.live/pages/datasets/list`) use the filter: + +* Name +* Search by + + +## Expected Result: + +- All filters must be working properly diff --git a/python-test/docs/development_guide.md b/python-test/docs/development_guide.md new file mode 100644 index 000000000..b1d037e45 --- /dev/null +++ b/python-test/docs/development_guide.md @@ -0,0 +1,184 @@ +## **INTEGRATION** + +| Integration Scenario | Automated via API | Automated via UI | Smoke | Sanity | +|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:-----------------:|:----------------:|:-----:|:------:| +| [Check if sink is active while scraping metrics](integration/sink_active_while_scraping_metrics.md) | ✅ | | 👍 | 👍 | +| [Check if sink with invalid credentials becomes active](integration/sink_error_invalid_credentials.md) | ✅ | | 👍 | 👍 | +| [Check if after 30 minutes without data sink becomes idle](integration/sink_idle_30_minutes.md) | | | | | +| [Provision agent before group (check if agent subscribes to the group)](integration/provision_agent_before_group.md) | ✅ | | 👍 | 👍 | +| [Provision agent after group (check if agent subscribes to the group)](integration/provision_agent_after_group.md) | ✅ | | 👍 | 👍 | +| [Provision agent with tag matching existing group linked to a valid dataset](integration/multiple_agents_subscribed_to_a_group.md) | ✅ | | 👍 | 👍 | +| [Apply multiple policies to a group](integration/apply_multiple_policies.md) | ✅ | | 👍 | 👍 | +| [Apply multiple policies to a group and remove one policy](integration/remove_one_of_multiple_policies.md) | ✅ | | 👍 | 👍 | +| [Apply multiple policies to a group and remove all of them](integration/remove_all_policies.md) | | | | | +| [Apply multiple policies to a group and remove one dataset](integration/remove_one_of_multiple_datasets.md) | ✅ | | 👍 | 👍 | +| [Apply multiple policies to a group and remove all datasets](integration/remove_all_datasets.md) | | | | | +| [Apply the same policy twice to the agent](integration/apply_policy_twice.md) | ✅ | | 👍 | 👍 | +| [Delete sink linked to a dataset, create another one and edit dataset using new sink](integration/change_sink_on_dataset.md) | | | | | +| [Remove one of multiples datasets that apply the same policy to the agent](integration/remove_one_dataset_of_multiples_with_same_policy.md) | | | | | +| [Remove group (invalid dataset, agent logs)](integration/remove_group.md) | ✅ | | 👍 | 👍 | +| [Remove sink (invalid dataset, agent logs)](integration/remove_sink.md) | | | 👍 | 👍 | +| [Remove policy (invalid dataset, agent logs, heartbeat)](integration/remove_policy.md) | ✅ | | 👍 | 👍 | +| [Remove dataset (check agent logs, heartbeat)](integration/remove_dataset.md) | ✅ | | 👍 | 👍 | +| [Remove agent container (logs, agent groups matches)](integration/remove_agent_container.md) | | | 👍 | 👍 | +| [Remove agent container force (logs, agent groups matches)](integration/remove_agent_container_force.md) | | | 👍 | 👍 | +| [Remove agent (logs, agent groups matches)](integration/remove_agent.md) | | | 👍 | 👍 | +| [Subscribe an agent to multiple groups created before agent provisioning](integration/subscribe_an_agent_to_multiple_groups_created_before_agent_provisioning.md) | ✅ | | 👍 | 👍 | +| [Subscribe an agent to multiple groups created after agent provisioning](integration/subscribe_an_agent_to_multiple_groups_created_after_agent_provisioning.md) | ✅ | | 👍 | 👍 | +| [Agent subscription to group after editing agent's tags (editing before agent provision)](integration/agent_subscription_to_group_after_editing_agent's_tags.md) | ✅ | | 👍 | 👍 | +| [Agent subscription to group after editing agent's tags (editing after agent provision and after groups creation)](integration/agent_subscription_to_group_after_editing_agent's_tags.md) | ✅ | | 👍 | 👍 | +| [Agent subscription to group after editing agent's tags (editing after agent provision and before second group creation)](integration/agent_subscription_to_group_after_editing_agent's_tags.md) | ✅ | | 👍 | 👍 | +| [Agent subscription to group with policies after editing agent's tags](integration/agent_subscription_to_group_with_policies_after_editing_agent's_tags.md) | ✅ | | 👍 | 👍 | +| [Agent subscription to multiple groups with policies after editing agent's tags](integration/agent_subscription_to_group_with_policies_after_editing_agent's_tags.md) | ✅ | | 👍 | 👍 | +| [Edit agent name and apply policies to then](integration/edit_agent_name_and_apply_policies_to_then.md) | ✅ | | 👍 | 👍 | +| [Insert tags in agents created without tags and apply policies to group matching new tags.md](integration/insert_tags_in_agents_created_without_tags_and_apply_policies_to_group_matching_new_tags.md) | ✅ | | 👍 | 👍 | + +--------------------------------- +## **LOGIN** + + +| Login Scenario | Automated via API | Automated via UI | Smoke | Sanity | +|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:-----------------:|:----------------:|:-----:|:------:| +| [Request registration of a registered account using registered password username and company](login/request_registration_of_a_registered_account_using_registered_password_username_and_company.md) | ✅ | | 👍 | 👍 | +| [Request registration of a registered account using registered password and username](login/request_registration_of_a_registered_account_using_registered_password_and_username.md) | ✅ | | 👍 | 👍 | +| [Request registration of a registered account using registered password and company](login/request_registration_of_a_registered_account_using_registered_password_and_company.md) | ✅ | | 👍 | 👍 | +| [Request registration of a registered account using registered password](login/request_registration_of_a_registered_account_using_registered_password.md) | ✅ | | 👍 | 👍 | +| [Request registration of a registered account using unregistered password username and company](login/request_registration_of_a_registered_account_using_unregistered_password_username_and_company.md) | ✅ | | 👍 | 👍 | +| [Request registration of a registered account using unregistered password and username](login/request_registration_of_a_registered_account_using_unregistered_password_and_username.md) | ✅ | | 👍 | 👍 | +| [Request registration of a registered account using unregistered password and company](login/request_registration_of_a_registered_account_using_unregistered_password_and_company.md) | ✅ | | 👍 | 👍 | +| [Request registration of a registered account using unregistered password](login/request_registration_of_a_registered_account_using_unregistered_password.md) | ✅ | | 👍 | 👍 | +| [Request registration of an unregistered account with valid password and invalid email](login/request_registration_of_an_unregistered_account_with_valid_password_and_invalid_email.md) | ✅ | | 👍 | 👍 | +| [Request registration of an unregistered account with valid password and valid email](login/request_registration_of_an_unregistered_account_with_valid_password_and_valid_email.md) | ✅ | | 👍 | 👍 | +| [Request registration of an unregistered account with invalid password and valid email](login/request_registration_of_an_unregistered_account_with_invalid_password_and_valid_email.md) | ✅ | | 👍 | 👍 | +| [Request registration of an unregistered account with invalid password and invalid email](login/request_registration_of_an_unregistered_account_with_invalid_password_and_invalid_email.md) | ✅ | | 👍 | 👍 | +| [Check if email and password are required fields](login/check_if_email_and_password_are_required_fields.md) | ✅ | | 👍 | 👍 | +| [Login with valid credentials](login/login_with_valid_credentials.md) | ✅ | ✅ | 👍 | 👍 | +| [Login with invalid credentials](login/login_with_invalid_credentials.md) | ✅ | | 👍 | 👍 | +| [Request password with registered email address](login/request_password_with_registered_email_address.md) | | | 👍 | 👍 | +| [Request password with unregistered email address](login/request_password_with_unregistered_email_address.md) | | | | 👍 | + +--------------------------------- +## **AGENTS** + + +| Agents Scenario | Automated via API | Automated via UI | Smoke | Sanity | +|:------------------------------------------------------------------------------------------------------------------------:|:-----------------:|:----------------:|:-----:|:------:| +| [Check if total agent on agents' page is correct](agents/check_if_total_agent_on_agents'_page_is_correct.md) | | | | | +| [Create agent without tags](agents/create_agent_without_tags.md) | ✅ | | | 👍 | +| [Create agent with one tag](agents/create_agent_with_one_tag.md) | ✅ | ✅ | 👍 | 👍 | +| [Create agent with multiple tags](agents/create_agent_with_multiple_tags.md) | ✅ | | | 👍 | +| [Create agent with invalid name (regex)](agents/create_agent_with_invalid_name_(regex).md) | | | | 👍 | +| [Create agent with duplicate name](agents/create_agent_with_duplicate_name.md) | | | | 👍 | +| [Test agent filters](agents/test_agent_filters.md) | | | | | +| [Check agent details](agents/check_agent_details.md) | | | | 👍 | +| [Edit an agent through the details modal](agents/edit_an_agent_through_the_details_modal.md) | | | | 👍 | +| [Edit agent name](agents/edit_agent_name.md) | ✅ | | 👍 | 👍 | +| [Edit agent tag](agents/edit_agent_tag.md) | ✅ | | 👍 | 👍 | +| [Edit agent name and tag](agents/edit_agent_name_and_tags.md) | ✅ | | | 👍 | +| [Save agent without tag](agents/save_agent_without_tag.md) | ✅ | | | 👍 | +| [Insert tags in agents created without tags](agents/insert_tags_in_agents_created_without_tags.md) | ✅ | | 👍 | 👍 | +| [Check if is possible cancel operations with no change](agents/check_if_is_possible_cancel_operations_with_no_change.md) | | | | | +| [Remove agent using correct name](agents/remove_agent_using_correct_name.md) | ✅ | | 👍 | 👍 | +| [Remove agent using incorrect name](agents/remove_agent_using_incorrect_name.md) | | | | 👍 | +| [Run two orb agents on the same port](agents/run_two_orb_agents_on_the_same_port.md) | ✅ | ✅ | 👍 | 👍 | +| [Run two orb agents on different ports](agents/run_two_orb_agents_on_different_ports.md) | ✅ | ✅ | 👍 | 👍 | + +--------------------------------- +## **AGENT GROUP** + + +| Agent Group Scenario | Automated via API | Automated via UI | Smoke | Sanity | +|:--------------------------------------------------------------------------------------------------------------------------------------------:|:-----------------:|:----------------:|:-----:|:------:| +| [Check if total agent groups on agent groups' page is correct](agent_groups/check_if_total_agent_groups_on_agent_groups'_page_is_correct.md) | | | | | +| [Create agent group with invalid name (regex)](agent_groups/create_agent_group_with_invalid_name_(regex).md) | | | | 👍 | +| [Create agent group with duplicate name](agent_groups/create_agent_group_with_duplicate_name.md) | | | | 👍 | +| [Create agent group with description](agent_groups/create_agent_group_with_description.md) | ✅ | | 👍 | 👍 | +| [Create agent group without description](agent_groups/create_agent_group_without_description.md) | | | | 👍 | +| [Create agent group without tag](agent_groups/create_agent_group_without_tag.md) | ✅ | | | 👍 | +| [Create agent group with one tag](agent_groups/create_agent_group_with_one_tag.md) | ✅ | | 👍 | 👍 | +| [Create agent group with multiple tags](agent_groups/create_agent_group_with_multiple_tags.md) | ✅ | | | 👍 | +| [Test agent groups filters](agent_groups/test_agent_groups_filters.md) | | | | | +| [Visualize matching agents](agent_groups/visualize_matching_agents.md) | | | | 👍 | +| [Check agent groups details](agent_groups/check_agent_groups_details.md) | | | | 👍 | +| [Edit an agent group through the details modal](agent_groups/edit_an_agent_group_through_the_details_modal.md) | | | | 👍 | +| [Check if is possible cancel operations with no change](agent_groups/check_if_is_possible_cancel_operations_with_no_change.md) | | | | | +| [Edit agent group name](agent_groups/edit_agent_group_name.md) | | | 👍 | 👍 | +| [Edit agent group description](agent_groups/edit_agent_group_description.md) | | | | 👍 | +| [Edit agent group tag](agent_groups/edit_agent_group_tag.md) | | | 👍 | 👍 | +| [Remove agent group using correct name](agent_groups/remove_agent_group_using_correct_name.md) | ✅ | | 👍 | 👍 | +| [Remove agent group using incorrect name](agent_groups/remove_agent_group_using_incorrect_name.md) | | | | 👍 | + + +--------------------------------- +## **SINK** + + +| Sink Scenario | Automated via API | Automated via UI | Smoke | Sanity | +|:---------------------------------------------------------------------------------------------------------------------------------------------------------------:|:-----------------:|:----------------:|:-----:|:------:| +| [Check if total sinks on sinks' page is correct](sinks/check_if_total_sinks_on_sinks'_page_is_correct.md) | | | | | +| [Create sink with invalid name (regex)](sinks/create_sink_with_invalid_name_(regex).md) | | | | 👍 | +| [Create sink with duplicate name](sinks/create_sink_with_duplicate_name.md) | | | | 👍 | +| [Create sink with description](sinks/create_sink_with_description.md) | ✅ | | 👍 | 👍 | +| [Create sink without description](sinks/create_sink_without_description.md) | | | | 👍 | +| [Create sink without tags](sinks/create_sink_without_tags.md) | ✅ | | 👍 | 👍 | +| [Create sink with tags](sinks/create_sink_with_tags.md) | | | | | +| [Create sink with multiple tags](sinks/create_sink_with_multiple_tags.md) | | | | | +| [Check if remote host, username and password are required to create a sink](sinks/check_if_remote_host,_username_and_password_are_required_to_create_a_sink.md) | | | | 👍 | +| [Test sink filters](sinks/test_sink_filters.md) | | | | | +| [Check sink details](sinks/check_sink_details.md) | | | | 👍 | +| [Edit a sink through the details modal](sinks/edit_a_sink_through_the_details_modal.md) | | | | 👍 | +| [Edit sink name](sinks/edit_sink_name.md) | | | | 👍 | +| [Edit sink description](sinks/edit_sink_description.md) | | | | 👍 | +| [Edit sink remote host](sinks/edit_sink_remote_host.md) | | | | 👍 | +| [Edit sink username](sinks/edit_sink_username.md) | | | | 👍 | +| [Edit sink password](sinks/edit_sink_password.md) | | | | 👍 | +| [Edit sink tags](sinks/edit_sink_tags.md) | | | | 👍 | +| [Check if is possible cancel operations with no change](sinks/check_if_is_possible_cancel_operations_with_no_change.md) | | | | | +| [Remove sink using correct name](sinks/remove_sink_using_correct_name.md) | ✅ | | 👍 | 👍 | +| [Remove sink using incorrect name](sinks/remove_sink_using_incorrect_name.md) | | | | 👍 | + + +--------------------------------- +## **POLICIES** + + +| Policies Scenario | Automated via API | Automated via UI | Smoke | Sanity | +|:--------------------------------------------------------------------------------------------------------------------------:|:-----------------:|:----------------:|:-----:|:------:| +| [Check if total policies on policies' page is correct](policies/check_if_total_policies_on_policies'_page_is_correct.md) | | | | | +| [Create policy with invalid name (regex)](policies/create_policy_with_invalid_name_(regex).md) | | | | 👍 | +| [Create policy with no agent provisioned](policies/create_policy_with_no_agent_provisioned.md) | | | | 👍 | +| [Create policy with duplicate name](policies/create_policy_with_duplicate_name.md) | | | | 👍 | +| [Create policy with description](policies/create_policy_with_description.md) | ✅ | | 👍 | 👍 | +| [Create policy without description](policies/create_policy_without_description.md) | | | | 👍 | +| [Create policy with dhcp handler](policies/create_policy_with_dhcp_handler.md) | ✅ | | 👍 | 👍 | +| [Create policy with dns handler](policies/create_policy_with_dns_handler.md) | ✅ | | 👍 | 👍 | +| [Create policy with net handler](policies/create_policy_with_net_handler.md) | ✅ | | 👍 | 👍 | +| [Create policy with multiple handlers](policies/create_policy_with_multiple_handlers.md) | | | | 👍 | +| [Test policy filters](policies/test_policy_filters.md) | | | | +| [Check policies details](policies/check_policies_details.md) | | | | 👍 | +| [Edit a policy through the details modal](policies/edit_a_policy_through_the_details_modal.md) | | | | 👍 | +| [Edit policy name](policies/edit_policy_name.md) | | | | 👍 | +| [Edit policy description](policies/edit_policy_description.md) | | | | 👍 | +| [Edit policy handler](policies/edit_policy_handler.md) | | | 👍 | 👍 | +| [Check if is possible cancel operations with no change](policies/check_if_is_possible_cancel_operations_with_no_change.md) | | | | | +| [Remove policy using correct name](policies/remove_policy_using_correct_name.md) | ✅ | | 👍 | 👍 | +| [Remove policy using incorrect name](policies/remove_policy_using_incorrect_name.md) | | | | 👍 | + + +--------------------------------- +## **DATASETS** + + +| Datasets Scenario | Automated via API | Automated via UI | Smoke | Sanity | +|:--------------------------------------------------------------------------------------------------------------------------:|:-----------------:|:----------------:|:-----:|:------:| +| [Check if total datasets on datasets' page is correct](datasets/check_if_total_datasets_on_datasets'_page_is_correct.md) | | | | | +| [Create dataset with invalid name (regex)](datasets/create_dataset_with_invalid_name_(regex).md) | | | | 👍 | +| [Create dataset](datasets/create_dataset.md) | ✅ | | 👍 | 👍 | +| [Check datasets details](datasets/check_datasets_details.md) | | | | 👍 | +| [Check if is possible cancel operations with no change](datasets/check_if_is_possible_cancel_operations_with_no_change.md) | | | | | +| [Test datasets filter](datasets/test_datasets_filter.md) | | | | | +| [Edit a dataset through the details modal](datasets/edit_a_dataset_through_the_details_modal.md) | | | | | +| [Edit dataset name](datasets/edit_dataset_name.md) | | | | 👍 | +| [Edit dataset sink](datasets/edit_dataset_sink.md) | | | | 👍 | +| [Remove dataset using correct name](datasets/remove_dataset_using_correct_name.md) | ✅ | | 👍 | 👍 | +| [Remove dataset using incorrect name](datasets/remove_dataset_using_incorrect_name.md) | | | | 👍 | \ No newline at end of file diff --git a/python-test/docs/img/ORB-logo-ring.png b/python-test/docs/img/ORB-logo-ring.png new file mode 100644 index 000000000..99a9f94e0 Binary files /dev/null and b/python-test/docs/img/ORB-logo-ring.png differ diff --git a/python-test/docs/index.md b/python-test/docs/index.md new file mode 100644 index 000000000..828e8a67a --- /dev/null +++ b/python-test/docs/index.md @@ -0,0 +1,165 @@ +## Login + +- [Request registration of a registered account using registered password username and company](login/request_registration_of_a_registered_account_using_registered_password_username_and_company.md) +- [Request registration of a registered account using registered password and username](login/request_registration_of_a_registered_account_using_registered_password_and_username.md) +- [Request registration of a registered account using registered password and company](login/request_registration_of_a_registered_account_using_registered_password_and_company.md) +- [Request registration of a registered account using registered password](login/request_registration_of_a_registered_account_using_registered_password.md) +- [Request registration of a registered account using unregistered password username and company](login/request_registration_of_a_registered_account_using_unregistered_password_username_and_company.md) +- [Request registration of a registered account using unregistered password and username](login/request_registration_of_a_registered_account_using_unregistered_password_and_username.md) +- [Request registration of a registered account using unregistered password and company](login/request_registration_of_a_registered_account_using_unregistered_password_and_company.md) +- [Request registration of a registered account using unregistered password](login/request_registration_of_a_registered_account_using_unregistered_password.md) +- [Request registration of an unregistered account with valid password and invalid email](login/request_registration_of_an_unregistered_account_with_valid_password_and_invalid_email.md) +- [Request registration of an unregistered account with valid password and valid email](login/request_registration_of_an_unregistered_account_with_valid_password_and_valid_email.md) +- [Request registration of an unregistered account with invalid password and valid email](login/request_registration_of_an_unregistered_account_with_invalid_password_and_valid_email.md) +- [Request registration of an unregistered account with invalid password and invalid email](login/request_registration_of_an_unregistered_account_with_invalid_password_and_invalid_email.md) +- [Check if email and password are required fields](login/check_if_email_and_password_are_required_fields.md) +- [Login with valid credentials](login/login_with_valid_credentials.md) +- [Login with invalid credentials](login/login_with_invalid_credentials.md) +- [Request password with registered email address](login/request_password_with_registered_email_address.md) +- [Request password with unregistered email address](login/request_password_with_unregistered_email_address.md) + + +## Agents + +- [Check if total agent on agents' page is correct](agents/check_if_total_agent_on_agents'_page_is_correct.md) +- [Create agent without tags](agents/create_agent_without_tags.md) +- [Create agent with one tag](agents/create_agent_with_one_tag.md) +- [Create agent with multiple tags](agents/create_agent_with_multiple_tags.md) +- [Create agent with invalid name (regex)](agents/create_agent_with_invalid_name_(regex).md) +- [Create agent with duplicate name](agents/create_agent_with_duplicate_name.md) +- [Test agent filters](agents/test_agent_filters.md) +- [Check agent details](agents/check_agent_details.md) +- [Edit an agent through the details modal](agents/edit_an_agent_through_the_details_modal.md) +- [Edit agent name](agents/edit_agent_name.md) +- [Edit agent tag](agents/edit_agent_tag.md) +- [Save agent without tag](agents/save_agent_without_tag.md) +- [Insert tags in agents created without tags](agents/insert_tags_in_agents_created_without_tags.md) +- [Check if is possible cancel operations with no change](agents/check_if_is_possible_cancel_operations_with_no_change.md) +- [Remove agent using correct name](agents/remove_agent_using_correct_name.md) +- [Remove agent using incorrect name](agents/remove_agent_using_incorrect_name.md) +- [Run two orb agents on the same port](agents/run_two_orb_agents_on_the_same_port.md) +- [Run two orb agents on different ports](agents/run_two_orb_agents_on_different_ports.md) +- [Edit agent name and tag](agents/edit_agent_name_and_tags.md) + + +## Agent Groups + +- [Check if total agent groups on agent groups' page is correct](agent_groups/check_if_total_agent_groups_on_agent_groups'_page_is_correct.md) +- [Create agent group with invalid name (regex)](agent_groups/create_agent_group_with_invalid_name_(regex).md) +- [Create agent group with duplicate name](agent_groups/create_agent_group_with_duplicate_name.md) +- [Create agent group with description](agent_groups/create_agent_group_with_description.md) +- [Create agent group without description](agent_groups/create_agent_group_without_description.md) +- [Create agent group without tag](agent_groups/create_agent_group_without_tag.md) +- [Create agent group with one tag](agent_groups/create_agent_group_with_one_tag.md) +- [Create agent group with multiple tags](agent_groups/create_agent_group_with_multiple_tags.md) +- [Test agent groups filters](agent_groups/test_agent_groups_filters.md) +- [Visualize matching agents](agent_groups/visualize_matching_agents.md) +- [Check agent groups details](agent_groups/check_agent_groups_details.md) +- [Edit an agent group through the details modal](agent_groups/edit_an_agent_group_through_the_details_modal.md) +- [Check if is possible cancel operations with no change](agent_groups/check_if_is_possible_cancel_operations_with_no_change.md) +- [Edit agent group name](agent_groups/edit_agent_group_name.md) +- [Edit agent group description](agent_groups/edit_agent_group_description.md) +- [Edit agent group tag](agent_groups/edit_agent_group_tag.md) +- [Remove agent group using correct name](agent_groups/remove_agent_group_using_correct_name.md) +- [Remove agent group using incorrect name](agent_groups/remove_agent_group_using_incorrect_name.md) + + +## Sinks + +- [Check if total sinks on sinks' page is correct](sinks/check_if_total_sinks_on_sinks'_page_is_correct.md) +- [Create sink with invalid name (regex)](sinks/create_sink_with_invalid_name_(regex).md) +- [Create sink with duplicate name](sinks/create_sink_with_duplicate_name.md) +- [Create sink with description](sinks/create_sink_with_description.md) +- [Create sink without description](sinks/create_sink_without_description.md) +- [Create sink without tags](sinks/create_sink_without_tags.md) +- [Create sink with tags](sinks/create_sink_with_tags.md) +- [Create sink with multiple tags](sinks/create_sink_with_multiple_tags.md) +- [Check if remote host, username and password are required to create a sink](sinks/check_if_remote_host,_username_and_password_are_required_to_create_a_sink.md) +- [Test sink filters](sinks/test_sink_filters.md) +- [Check sink details](sinks/check_sink_details.md) +- [Edit a sink through the details modal](sinks/edit_a_sink_through_the_details_modal.md) +- [Edit sink name](sinks/edit_sink_name.md) +- [Edit sink description](sinks/edit_sink_description.md) +- [Edit sink remote host](sinks/edit_sink_remote_host.md) +- [Edit sink username](sinks/edit_sink_username.md) +- [Edit sink password](sinks/edit_sink_password.md) +- [Edit sink tags](sinks/edit_sink_tags.md) +- [Check if is possible cancel operations with no change](sinks/check_if_is_possible_cancel_operations_with_no_change.md) +- [Remove sink using correct name](sinks/remove_sink_using_correct_name.md) +- [Remove sink using incorrect name](sinks/remove_sink_using_incorrect_name.md) + +## Policies + +- [Check if total policies on policies' page is correct](policies/check_if_total_policies_on_policies'_page_is_correct.md) +- [Create policy with invalid name (regex)](policies/create_policy_with_invalid_name_(regex).md) +- [Create policy with no agent provisioned](policies/create_policy_with_no_agent_provisioned.md) +- [Create policy with duplicate name](policies/create_policy_with_duplicate_name.md) +- [Create policy with description](policies/create_policy_with_description.md) +- [Create policy without description](policies/create_policy_without_description.md) +- [Create policy with dhcp handler](policies/create_policy_with_dhcp_handler.md) +- [Create policy with dns handler](policies/create_policy_with_dns_handler.md) +- [Create policy with net handler](policies/create_policy_with_net_handler.md) +- [Create policy with multiple handlers](policies/create_policy_with_multiple_handlers.md) +- [Test policy filters](policies/test_policy_filters.md) +- [Check policies details](policies/check_policies_details.md) +- [Edit a policy through the details modal](policies/edit_a_policy_through_the_details_modal.md) +- [Edit policy name](policies/edit_policy_name.md) +- [Edit policy description](policies/edit_policy_description.md) +- [Edit policy handler](policies/edit_policy_handler.md) +- [Check if is possible cancel operations with no change](policies/check_if_is_possible_cancel_operations_with_no_change.md) +- [Remove policy using correct name](policies/remove_policy_using_correct_name.md) +- [Remove policy using incorrect name](policies/remove_policy_using_incorrect_name.md) + +## Datasets + +- [Check if total datasets on datasets' page is correct](datasets/check_if_total_datasets_on_datasets'_page_is_correct.md) +- [Create dataset with invalid name (regex)](datasets/create_dataset_with_invalid_name_(regex).md) +- [Create dataset](datasets/create_dataset.md) +- [Check datasets details](datasets/check_datasets_details.md) +- [Check if is possible cancel operations with no change](datasets/check_if_is_possible_cancel_operations_with_no_change.md) +- [Test datasets filter](datasets/test_datasets_filter.md) +- [Edit a dataset through the details modal](datasets/edit_a_dataset_through_the_details_modal.md) +- [Edit dataset name](datasets/edit_dataset_name.md) +- [Edit dataset sink](datasets/edit_dataset_sink.md) +- [Remove dataset using correct name](datasets/remove_dataset_using_correct_name.md) +- [Remove dataset using incorrect name](datasets/remove_dataset_using_incorrect_name.md) + +## Integration tests + +- [Check if sink is active while scraping metrics](integration/sink_active_while_scraping_metrics.md) +- [Check if sink with invalid credentials becomes active](integration/sink_error_invalid_credentials.md) +- [Check if after 30 minutes without data sink becomes idle](integration/sink_idle_30_minutes.md) +- [Provision agent before group (check if agent subscribes to the group)](integration/provision_agent_before_group.md) +- [Provision agent after group (check if agent subscribes to the group)](integration/provision_agent_after_group.md) +- [Create agent with tag matching existing group linked to a valid dataset](integration/multiple_agents_subscribed_to_a_group.md) +- [Apply multiple policies to a group](integration/apply_multiple_policies.md) +- [Apply multiple policies to a group and remove one policy](integration/remove_one_of_multiple_policies.md) +- [Apply multiple policies to a group and remove all of them](integration/remove_all_policies.md) +- [Apply multiple policies to a group and remove one dataset](integration/remove_one_of_multiple_datasets.md) +- [Apply multiple policies to a group and remove all datasets](integration/remove_all_datasets.md) +- [Apply the same policy twice to the agent](integration/apply_policy_twice.md) +- [Delete sink linked to a dataset, create another one and edit dataset using new sink](integration/change_sink_on_dataset.md) +- [Remove one of multiples datasets that apply the same policy to the agent](integration/remove_one_dataset_of_multiples_with_same_policy.md) +- [Remove group (invalid dataset, agent logs)](integration/remove_group.md) +- [Remove sink (invalid dataset, agent logs)](integration/remove_sink.md) +- [Remove policy (invalid dataset, agent logs, heartbeat)](integration/remove_policy.md) +- [Remove dataset (check agent logs, heartbeat)](integration/remove_dataset.md) +- [Remove agent container (logs, agent groups matches)](integration/remove_agent_container.md) +- [Remove agent container force (logs, agent groups matches)](integration/remove_agent_container_force.md) +- [Remove agent (logs, agent groups matches)](integration/remove_agent.md) +- [Subscribe an agent to multiple groups created before agent provisioning](integration/subscribe_an_agent_to_multiple_groups_created_before_agent_provisioning.md) +- [Subscribe an agent to multiple groups created after agent provisioning](integration/subscribe_an_agent_to_multiple_groups_created_after_agent_provisioning.md) +- [Agent subscription to group after editing agent's tags](integration/agent_subscription_to_group_after_editing_agent's_tags.md) +- [Agent subscription to group with policies after editing agent's tags](integration/agent_subscription_to_group_with_policies_after_editing_agent's_tags.md) +- [Edit agent name and apply policies to then](integration/edit_agent_name_and_apply_policies_to_then.md) +- [Insert tags in agents created without tags and apply policies to group matching new tags.md](integration/insert_tags_in_agents_created_without_tags_and_apply_policies_to_group_matching_new_tags.md) + +## Pktvisor Agent + +* Providing Orb-agent with sample commands +* Providing Orb-agent with configuration files +* Providing Orb-agent with advanced auto-provisioning setup +* Providing more than one Orb-agent with different ports +* Providing Orb-agent using mocking interface +* Providing a Orb-agent with a wrong interface +* Pull the latest orb-agent image, build and run the agent \ No newline at end of file diff --git a/python-test/docs/integration/agent_subscription_to_group_after_editing_agent's_tags.md b/python-test/docs/integration/agent_subscription_to_group_after_editing_agent's_tags.md new file mode 100644 index 000000000..581781b9e --- /dev/null +++ b/python-test/docs/integration/agent_subscription_to_group_after_editing_agent's_tags.md @@ -0,0 +1,43 @@ +## Scenario: Agent subscription to group after editing agent's tags (agent provisioned before group) +Steps: +- +1. Provision an agent with tags +2. Create a group with same tags as agent +3. Create another group with different tags +4. Edit agent orb tags to match with second group + +Expected result: +- +- Agent heartbeat must show just one group matching +- Agent logs must show that agent is unsubscribed from the first group +- Agent logs must show that agent is subscribed to the second group + + +## Scenario: Agent subscription to group after editing agent's tags (agent provisioned after group) +Steps: +- +1. Create a group with tags +2. Provision an agent with same tags +3. Create another group with different tags +4. Edit agent orb tags to match with second group + +Expected result: +- +- Agent heartbeat must show just one group matching +- Agent logs must show that agent is unsubscribed from the first group +- Agent logs must show that agent is subscribed to the second group + + +## Scenario: Agent subscription to group after editing agent's tags (agent provisioned after groups) +Steps: +- +1. Create a group with tags +2. Create another group with different tags +3. Provision an agent with same tags as first group +4. Edit agent orb tags to match with second group + +Expected result: +- +- Agent heartbeat must show just one group matching +- Agent logs must show that agent is unsubscribed from the first group +- Agent logs must show that agent is subscribed to the second group \ No newline at end of file diff --git a/python-test/docs/integration/agent_subscription_to_group_with_policies_after_editing_agent's_tags.md b/python-test/docs/integration/agent_subscription_to_group_with_policies_after_editing_agent's_tags.md new file mode 100644 index 000000000..37f045101 --- /dev/null +++ b/python-test/docs/integration/agent_subscription_to_group_with_policies_after_editing_agent's_tags.md @@ -0,0 +1,40 @@ +## Scenario: Agent subscription to group with policies after editing agent's tags +Steps: +- +1. Provision an agent with tags +2. Create a group with same tags as agent +3. Create a sink +4. Create 1 policy +5. Create a dataset linking the group, the sink and the policy +6. Create another group with different tags +7. Create another policy and apply to the group +8. Edit agent orb tags to match with second group + +Expected result: +- +- Agent heartbeat must show just one group matching +- Agent logs must show that agent is unsubscribed from the first group +- Agent logs must show that agent is subscribed to the second group +- The container logs contain the message "policy applied successfully" referred to the policy applied to the second group +- The container logs that were output after all policies have been applied contains the message "scraped metrics for policy" referred to each applied policy + + +## Scenario: Agent subscription to multiple groups with policies after editing agent's tags +Steps: +- +1. Provision an agent with tags +2. Create a group with same tags as agent +3. Create a sink +4. Create 1 policy +5. Create a dataset linking the group, the sink and the policy +6. Create another group with different tags +7. Create another policy and apply to the group +8. Edit agent orb tags to match with both groups + +Expected result: +- +- Agent heartbeat must show 2 group matching +- Agent logs must show that agent is unsubscribed from the first group +- Agent logs must show that agent is subscribed to the second group +- The container logs contain the message "policy applied successfully" referred to the policy applied to both groups +- The container logs that were output after all policies have been applied contains the message "scraped metrics for policy" referred to each applied policy diff --git a/python-test/docs/integration/apply_multiple_policies.md b/python-test/docs/integration/apply_multiple_policies.md new file mode 100644 index 000000000..f629a8af4 --- /dev/null +++ b/python-test/docs/integration/apply_multiple_policies.md @@ -0,0 +1,18 @@ +## Scenario: apply multiple policies to agents subscribed to a group + +Steps: +- +1. Provision an agent with tags +2. Create a group with same tags as agent +3. Create a sink +4. Create 2 policies +5. Create a dataset linking the group, the sink and one of the policies +6. Create another dataset linking the same group, sink and the other policy + +Expected result: +- +- All the policies must be applied to the agent (orb-agent API response) +- The container logs contain the message "policy applied successfully" referred to each policy +- The container logs that were output after all policies have been applied contain the message "scraped metrics for policy" referred to each applied policy +- Referred sink must have active state on response +- Datasets related to all existing policies have validity valid \ No newline at end of file diff --git a/python-test/docs/integration/apply_policy_twice.md b/python-test/docs/integration/apply_policy_twice.md new file mode 100644 index 000000000..b3ce45972 --- /dev/null +++ b/python-test/docs/integration/apply_policy_twice.md @@ -0,0 +1,18 @@ +## Scenario: apply twice the same policy to agents subscribed to a group + +Steps: +- +1. Provision an agent with tags +2. Create a group with same tags as agent +3. Create a sink +4. Create 1 policy +5. Create a dataset linking the group, the sink and the policy +6. Create another dataset linking the same group and policy (sink can be the same or a different one) + +Expected result: +- +- The policy must be applied to the agent (orb-agent API response) and two datasets must be listed linked to the policy +- The container logs contain the message "policy applied successfully" referred to the policy +- The container logs contain the message "scraped metrics for policy" referred to the policy +- All sinks linked must have active state on response +- Both datasets have validity valid \ No newline at end of file diff --git a/python-test/docs/integration/change_sink_on_dataset.md b/python-test/docs/integration/change_sink_on_dataset.md new file mode 100644 index 000000000..13141eb0b --- /dev/null +++ b/python-test/docs/integration/change_sink_on_dataset.md @@ -0,0 +1,20 @@ +## Scenario: edit sink on dataset + +Steps: +- +1. Provision an agent with tags +2. Create a group with same tags as agent +3. Create 2 sinks +4. Create 1 policy +5. Create a dataset linking the group, one of the sinks and the policy +6. Wait for scraping metrics for policy +7. Edit the dataset changing the sink + +Expected result: +- +- The policy must be applied to the agent (orb-agent API response) +- The container logs contain the message "policy applied successfully" referred to the policy +- The container logs contain the message "scraped metrics for policy" referred to the policy +- Datasets have validity valid +- First applied sink must stop to receive data after the edition +- Second applied sink must start to receive data after the edition \ No newline at end of file diff --git a/python-test/docs/integration/edit_agent_name_and_apply_policies_to_then.md b/python-test/docs/integration/edit_agent_name_and_apply_policies_to_then.md new file mode 100644 index 000000000..dc485b6ac --- /dev/null +++ b/python-test/docs/integration/edit_agent_name_and_apply_policies_to_then.md @@ -0,0 +1,16 @@ +## Scenario: Edit agent name and apply policies to then +Steps: +- +1. Provision an agent with tags +2. Create a group with same tags as agent +3. Create a sink +4. Create 1 policy +5. Create a dataset linking the group, the sink and the policy +8. Edit agent name + +Expected result: +- +- Agent heartbeat must show just one group matching +- Agent logs must show that agent is subscribed to the group +- The container logs contain the message "policy applied successfully" referred to the policy applied to the second group +- The container logs that were output after all policies have been applied contains the message "scraped metrics for policy" referred to each applied policy diff --git a/python-test/docs/integration/insert_tags_in_agents_created_without_tags_and_apply_policies_to_group_matching_new_tags.md b/python-test/docs/integration/insert_tags_in_agents_created_without_tags_and_apply_policies_to_group_matching_new_tags.md new file mode 100644 index 000000000..5a44f4905 --- /dev/null +++ b/python-test/docs/integration/insert_tags_in_agents_created_without_tags_and_apply_policies_to_group_matching_new_tags.md @@ -0,0 +1,15 @@ +## Scenario: Insert tags in agents created without tags and apply policies to group matching new tags +## Steps: +1. Provision an agent without tags +2. Create a group with tags +3. Create a sink +4. Create 1 policy +5. Create a dataset linking the group, the sink and the policy +8. Edit agent name and tags, using the same tag as the group + +Expected result: +- +- Agent heartbeat must show just one group matching +- Agent logs must show that agent is subscribed to the group +- The container logs contain the message "policy applied successfully" referred to the policy applied to the second group +- The container logs that were output after all policies have been applied contains the message "scraped metrics for policy" referred to each applied policy diff --git a/python-test/docs/integration/multiple_agents_subscribed_to_a_group.md b/python-test/docs/integration/multiple_agents_subscribed_to_a_group.md new file mode 100644 index 000000000..c7522925c --- /dev/null +++ b/python-test/docs/integration/multiple_agents_subscribed_to_a_group.md @@ -0,0 +1,18 @@ +## Scenario: multiple agents subscribed to a group + +Steps: +- +1. Provision an agent with tags +2. Provision another agent with same tags (use var env `ORB_BACKENDS_PKTVISOR_API_PORT` to change pktvisor port) +3. Create a group with same tags as agents +4. Create a sink +5. Create 1 policy +6. Create a dataset linking the group, the sink and the policy + +Expected result: +- +- The policy must be applied to both agents (orb-agent API response) +- The container logs contain the message "policy applied successfully" referred to the policy +- The container logs contain the message "scraped metrics for policy" referred to the policy +- Referred sink must have active state on response +- Dataset must have validity valid \ No newline at end of file diff --git a/python-test/docs/integration/provision_agent_after_group.md b/python-test/docs/integration/provision_agent_after_group.md new file mode 100644 index 000000000..281f1e494 --- /dev/null +++ b/python-test/docs/integration/provision_agent_after_group.md @@ -0,0 +1,12 @@ +## Scenario: provision an agent after create an agent group + +Steps: +- +1. Create a group with tags +2. Provision an agent with same tags as group + +Expected result: +- +- The orb-agent container logs contain the message "completed RPC subscription to group" +- Group has one agent matching +- Agent status is online \ No newline at end of file diff --git a/python-test/docs/integration/provision_agent_before_group.md b/python-test/docs/integration/provision_agent_before_group.md new file mode 100644 index 000000000..27726d5d8 --- /dev/null +++ b/python-test/docs/integration/provision_agent_before_group.md @@ -0,0 +1,12 @@ +## Scenario: provision an agent before create an agent group + +Steps: +- +1. Provision an agent with tags +2. Create a group with same tags as the agent + +Expected result: +- +- The orb-agent container logs contain the message "completed RPC subscription to group" +- Group has one agent matching +- Agent status is online \ No newline at end of file diff --git a/python-test/docs/integration/remove_agent.md b/python-test/docs/integration/remove_agent.md new file mode 100644 index 000000000..36e9223db --- /dev/null +++ b/python-test/docs/integration/remove_agent.md @@ -0,0 +1,15 @@ +## Scenario: Remove agent + +Steps: +- +1. Provision an agent with tags +2. Create a group with same tags as agent +3. Create a sink +4. Create a policy +5. Create a dataset linking the group, the sink and the policy +6. Remove agent from orb + +Expected result: +- +- Orb-agent logs should not have any error +- Group must match 0 agents \ No newline at end of file diff --git a/python-test/docs/integration/remove_agent_container.md b/python-test/docs/integration/remove_agent_container.md new file mode 100644 index 000000000..ac62b1aeb --- /dev/null +++ b/python-test/docs/integration/remove_agent_container.md @@ -0,0 +1,23 @@ +## Scenario: Remove agent container + +Steps: +- +1. Provision an agent with tags +2. Create a group with same tags as agent +3. Create a sink +4. Create a policy +5. Create a dataset linking the group, the sink and the policy +6. Stop and remove orb-agent container + +Expected result: +- +- The orb-agent container logs contain: +``` +{"level":"info","ts":"time","caller":"pktvisor/pktvisor.go:390","msg":"pktvisor stopping"} +{"l/pktvisor.go:253","msg":"pktvisor stdout","log": "Shutting down"} +{"level":"info","ts":"time","caller":"pktvisor/pktvisor.go:253","msg":"pktvisor stdout","log": "policy [policy_name]": "stopping"} +{"level":"info","ts":"time","caller":"pktvisor/pktvisor.go:253","msg":"pktvisor stdout","log": "policy [policy_name]": "stopping input instance: "} +{"level":"info","ts":"time","caller":"pktvisor/pktvisor.go:253","msg":"pktvisor stdout","log": "policy [policy_name]": "stopping handler instance: "} +{"level":"info","ts":"time","caller":"pktvisor/pktvisor.go:253","msg":"pktvisor stdout","log": "exit with success"} +``` +- Logs should not have any error diff --git a/python-test/docs/integration/remove_agent_container_force.md b/python-test/docs/integration/remove_agent_container_force.md new file mode 100644 index 000000000..26daf78c0 --- /dev/null +++ b/python-test/docs/integration/remove_agent_container_force.md @@ -0,0 +1,23 @@ +## Scenario: Remove agent container without stop + +Steps: +- +1. Provision an agent with tags +2. Create a group with same tags as agent +3. Create a sink +4. Create a policy +5. Create a dataset linking the group, the sink and the policy +6. Remove orb-agent container + +Expected result: +- +- The orb-agent container logs contain: +``` +{"level":"info","ts":"time","caller":"pktvisor/pktvisor.go:390","msg":"pktvisor stopping"} +{"l/pktvisor.go:253","msg":"pktvisor stdout","log": "Shutting down"} +{"level":"info","ts":"time","caller":"pktvisor/pktvisor.go:253","msg":"pktvisor stdout","log": "policy [policy_name]": "stopping"} +{"level":"info","ts":"time","caller":"pktvisor/pktvisor.go:253","msg":"pktvisor stdout","log": "policy [policy_name]": "stopping input instance: "} +{"level":"info","ts":"time","caller":"pktvisor/pktvisor.go:253","msg":"pktvisor stdout","log": "policy [policy_name]": "stopping handler instance: "} +{"level":"info","ts":"time","caller":"pktvisor/pktvisor.go:253","msg":"pktvisor stdout","log": "exit with success"} +``` +- Logs should not have any error \ No newline at end of file diff --git a/python-test/docs/integration/remove_all_datasets.md b/python-test/docs/integration/remove_all_datasets.md new file mode 100644 index 000000000..9fbbf387d --- /dev/null +++ b/python-test/docs/integration/remove_all_datasets.md @@ -0,0 +1,18 @@ +## Scenario: remove all datasets from an agent + +Steps: +- +1. Provision an agent with tags +2. Create a group with same tags as agent +3. Create a sink +4. Create 2 policies +5. Create a dataset linking the group, the sink and one of the policies +6. Create another dataset linking the same group, sink and the other policy +7. Create third dataset applying one of the policies again +8. Remove all datasets + +Expected result: +- +- The agent's heartbeat shows that 0 policies are applied +- Container logs should inform that removed policy was stopped and removed +- Container logs that were output after removing dataset does not contain the message "scraped metrics for policy" referred to deleted policies anymore \ No newline at end of file diff --git a/python-test/docs/integration/remove_all_policies.md b/python-test/docs/integration/remove_all_policies.md new file mode 100644 index 000000000..e090a5440 --- /dev/null +++ b/python-test/docs/integration/remove_all_policies.md @@ -0,0 +1,18 @@ +## Scenario: remove all policies from an agent + +Steps: +- +1. Provision an agent with tags +2. Create a group with same tags as agent +3. Create a sink +4. Create 2 policies +5. Create a dataset linking the group, the sink and one of the policies +6. Create another dataset linking the same group, sink and the other policy +7. Remove both policies + +Expected result: +- +- The agent's heartbeat shows that 0 policies are applied +- Container logs should inform that removed policy was stopped and removed +- Container logs that were output after removing policies does not contain the message "scraped metrics for policy" referred to deleted policies anymore +- Datasets became "invalid" \ No newline at end of file diff --git a/python-test/docs/integration/remove_dataset.md b/python-test/docs/integration/remove_dataset.md new file mode 100644 index 000000000..1f6b276c7 --- /dev/null +++ b/python-test/docs/integration/remove_dataset.md @@ -0,0 +1,16 @@ +## Scenario: remove dataset from an agent + +Steps: +- +1. Provision an agent with tags +2. Create a group with same tags as agent +3. Create a sink +4. Create 1 policy +5. Create a dataset linking the group, the sink and the policy +7. Remove the dataset + +Expected result: +- +- The agent's heartbeat shows that 0 policies are applied +- Container logs should inform that removed policy was stopped and removed +- Container logs that were output after removing dataset does not contain the message "scraped metrics for policy" referred to deleted policy anymore \ No newline at end of file diff --git a/python-test/docs/integration/remove_group.md b/python-test/docs/integration/remove_group.md new file mode 100644 index 000000000..e69de29bb diff --git a/python-test/docs/integration/remove_one_dataset_of_multiples_with_same_policy.md b/python-test/docs/integration/remove_one_dataset_of_multiples_with_same_policy.md new file mode 100644 index 000000000..94bc79e9e --- /dev/null +++ b/python-test/docs/integration/remove_one_dataset_of_multiples_with_same_policy.md @@ -0,0 +1,16 @@ +## Scenario: remove one of multiple datasets with same policy + +Steps: +- +1. Provision an agent with tags +2. Create a group with same tags as agent +3. Create a sink +4. Create 1 policy +5. Create a dataset linking the group, the sink and the policy +6. Create another dataset linking the same group and policy (sink can be the same or a different one) +7. Remove one of the datasets + +Expected result: +- +- The agent's heartbeat shows that 1 policies are applied +- The orb agent container logs that were output after removing dataset contain the message "scraped metrics for policy" referred to the applied policy \ No newline at end of file diff --git a/python-test/docs/integration/remove_one_of_multiple_datasets.md b/python-test/docs/integration/remove_one_of_multiple_datasets.md new file mode 100644 index 000000000..a899f579e --- /dev/null +++ b/python-test/docs/integration/remove_one_of_multiple_datasets.md @@ -0,0 +1,18 @@ +## Scenario: remove one of multiple datasets + +Steps: +- +1. Provision an agent with tags +2. Create a group with same tags as agent +3. Create a sink +4. Create 2 policies +5. Create a dataset linking the group, the sink and one of the policies +6. Create another dataset linking the same group, sink and the other policy +7. Remove one of the datasets + +Expected result: +- +- The agent's heartbeat shows that 1 policies are applied +- The orb-agent container logs should inform that removed policy was stopped and removed +- The orb-agent container logs that were output after removing dataset contain the message "scraped metrics for policy" referred to applied policy +- The orb-agent container logs that were output after removing dataset does not contain the message "scraped metrics for policy" referred to deleted policy anymore \ No newline at end of file diff --git a/python-test/docs/integration/remove_one_of_multiple_policies.md b/python-test/docs/integration/remove_one_of_multiple_policies.md new file mode 100644 index 000000000..2cc1fd3f8 --- /dev/null +++ b/python-test/docs/integration/remove_one_of_multiple_policies.md @@ -0,0 +1,20 @@ +## Scenario: remove one of multiple policies from an agent + +Steps: +- +1. Provision an agent with tags +2. Create a group with same tags as agent +3. Create a sink +4. Create 2 policies +5. Create a dataset linking the group, the sink and one of the policies +6. Create another dataset linking the same group, sink and the other policy +7. Remove 1 policy + +Expected result: +- +- The agent's heartbeat shows that 1 policies are applied +- Container logs should inform that removed policy was stopped and removed +- Container logs that were output after removing policies does not contain the message "scraped metrics for policy" referred to deleted policy anymore +- The orb agent container logs that were output after removing policy contain the message "scraped metrics for policy" referred to the remained policy +- Dataset referred to removed policy became "invalid" +- Dataset referred to remained policy remains "valid" \ No newline at end of file diff --git a/python-test/docs/integration/remove_policy.md b/python-test/docs/integration/remove_policy.md new file mode 100644 index 000000000..df75e29aa --- /dev/null +++ b/python-test/docs/integration/remove_policy.md @@ -0,0 +1,17 @@ +## Scenario: remove policy from an agent + +Steps: +- +1. Provision an agent with tags +2. Create a group with same tags as agent +3. Create a sink +4. Create 1 policy +5. Create a dataset linking the group, the sink and one of the policies +7. Remove the policy + +Expected result: +- +- The agent's heartbeat shows that 0 policies are applied +- Container logs should inform that removed policy was stopped and removed +- Container logs that were output after removing policies does not contain the message "scraped metrics for policy" referred to deleted policy anymore +- Datasets became "invalid" \ No newline at end of file diff --git a/python-test/docs/integration/remove_sink.md b/python-test/docs/integration/remove_sink.md new file mode 100644 index 000000000..e69de29bb diff --git a/python-test/docs/integration/sink_active_while_scraping_metrics.md b/python-test/docs/integration/sink_active_while_scraping_metrics.md new file mode 100644 index 000000000..a30b97921 --- /dev/null +++ b/python-test/docs/integration/sink_active_while_scraping_metrics.md @@ -0,0 +1,35 @@ +## Scenario: sink has active status while scraping metrics + +Steps: +- +1. Provision an agent with tags +2. Create a group with same tags as agent +3. Create a sink with valid credentials +4. Create 1 policy +5. Create a dataset linking the group, the sink and one of the policies +6. Wait 1 minute + +Expected result: +- +- The container logs contain the message "scraped metrics for policy" referred to the applied policy +- Sink status must be "active" + +
+------------------------------------------------- + +## Advanced Scenario: sink has active status while scraping metrics + +Steps: +- +1. Provision an agent with tags +2. Create a group with same tags as agent +3. Create a sink +4. Create 1 policy +5. Create a dataset linking the group, the sink and one of the policies +6. Keep sending data for 24 hours + + +Expected result: +- +- The container logs contain the message "scraped metrics for policy" referred to the applied policy +- Check if even after this time, sink status remains active \ No newline at end of file diff --git a/python-test/docs/integration/sink_error_invalid_credentials.md b/python-test/docs/integration/sink_error_invalid_credentials.md new file mode 100644 index 000000000..d2dcb44fd --- /dev/null +++ b/python-test/docs/integration/sink_error_invalid_credentials.md @@ -0,0 +1,15 @@ +## Scenario: sink has error status if credentials are invalid + +Steps: +- +1. Provision an agent with tags +2. Create a group with same tags as agent +3. Create a sink with invalid credentials +4. Create 1 policy +5. Create a dataset linking the group, the sink and one of the policies +6. Wait 1 minute + +Expected result: +- +- The container logs contain the message "scraped metrics for policy" referred to the applied policy +- Sink status must be "error" \ No newline at end of file diff --git a/python-test/docs/integration/sink_idle_30_minutes.md b/python-test/docs/integration/sink_idle_30_minutes.md new file mode 100644 index 000000000..d0dcf2b82 --- /dev/null +++ b/python-test/docs/integration/sink_idle_30_minutes.md @@ -0,0 +1,16 @@ +## Scenario: sink has idle status after 30 minutes without data + +Steps: +- +1. Provision an agent with tags +2. Create a group with same tags as agent +3. Create a sink with invalid credentials +4. Create 1 policy +5. Create a dataset linking the group, the sink and one of the policies +6. Wait 1 minute +7. Remove the dataset to which sink is linked +8. Wait 30 minutes + +Expected result: +- +- Sink status must be "idle" \ No newline at end of file diff --git a/python-test/docs/integration/subscribe_an_agent_to_multiple_groups_created_after_agent_provisioning.md b/python-test/docs/integration/subscribe_an_agent_to_multiple_groups_created_after_agent_provisioning.md new file mode 100644 index 000000000..2dafb31b1 --- /dev/null +++ b/python-test/docs/integration/subscribe_an_agent_to_multiple_groups_created_after_agent_provisioning.md @@ -0,0 +1,12 @@ +## Scenario: Subscribe an agent to multiple groups created after agent provisioning + +## Steps: +1. Provision an agent with tags +2. Create a group with at least one tag equal to agent +3. Create another group with at least one tag equal to agent +4. Check agent's logs and agent's heartbeat + + +## Expected Result: +1 - Logs must display the message "completed RPC subscription to group" referred to both groups +2 - Agent's heartbeat must have 2 groups linked diff --git a/python-test/docs/integration/subscribe_an_agent_to_multiple_groups_created_before_agent_provisioning.md b/python-test/docs/integration/subscribe_an_agent_to_multiple_groups_created_before_agent_provisioning.md new file mode 100644 index 000000000..67ce5639d --- /dev/null +++ b/python-test/docs/integration/subscribe_an_agent_to_multiple_groups_created_before_agent_provisioning.md @@ -0,0 +1,11 @@ +## Scenario: Subscribe an agent to multiple groups created before agent provisioning +## Steps: +1. Create a group with one tag +2. Create another group with 2 tags +3. Provision an agent with the same tags as the two groups +4. Check agent's logs and agent's heartbeat + + +## Expected Result: +1 - Logs must display the message "completed RPC subscription to group" referred to both groups +2 - Agent's heartbeat must have 2 groups linked \ No newline at end of file diff --git a/python-test/docs/login/check_if_email_and_password_are_required_fields.md b/python-test/docs/login/check_if_email_and_password_are_required_fields.md new file mode 100644 index 000000000..231a1bd7f --- /dev/null +++ b/python-test/docs/login/check_if_email_and_password_are_required_fields.md @@ -0,0 +1,45 @@ +## Scenario: Check if email and password are required fields + +### Sub Scenarios: +## I - Check if email is a required field + +### Steps: + +1 - Request an account registration without email field + +- REST API Method: POST +- endpoint: /users +- body: `{"password":"password", "metadata":{"company":"company","fullName":"name"}}` + +### Expected Result: + +- The request must fail with bad request (error 400) and no account must be registered + +## II - Check if password is a required field + +### Steps: + +1- Request an account registration without password field + +- REST API Method: POST +- endpoint: /users +- body: `{"email":"email", "metadata":{"company":"company","fullName":"name"}}` + +### Expected Result: + +- The request must fail with bad request (error 400) and no account must be registered + + +## III - Check if password and email are required fields + +### Steps: + +1 - Request an account registration using just metadata + +- REST API Method: POST +- endpoint: /users +- body: `{"metadata":{"company":"company","fullName":"name"}}` + +### Expected Result: + +- The request must fail with bad request (error 400) and no account must be registered \ No newline at end of file diff --git a/python-test/docs/login/login_with_invalid_credentials.md b/python-test/docs/login/login_with_invalid_credentials.md new file mode 100644 index 000000000..6e41c6219 --- /dev/null +++ b/python-test/docs/login/login_with_invalid_credentials.md @@ -0,0 +1,45 @@ +## Scenario: Login with invalid credentials + +### Sub Scenarios: +## I - Login with invalid email + +## Steps: + +1- Request authentication token using unregistered email and some registered password + +- REST API Method: POST +- endpoint: /tokens +- body: `{"email": "invalid_email", "password": "password"}` + + +## Expected Result: + - The request must fail with forbidden (error 403) and no token must be generated + +## II - Login with invalid password + +## Steps: + +1- Request authentication token using registered email and wrong password + +- REST API Method: POST +- endpoint: /tokens +- body: `{"email": "email", "password": "wrong_password"}` + + +## Expected Result: +- The request must fail with forbidden (error 403) and no token must be generated + +## III - Login with invalid email and invalid password + +## Steps: + +1- Request authentication token using unregistered email and unregistered password + +- REST API Method: POST +- endpoint: /tokens +- body: `{"email": "invalid_email", "password": "invalid_password"}` + + +## Expected Result: + +- The request must fail with forbidden (error 403) and no token must be generated \ No newline at end of file diff --git a/python-test/docs/login/login_with_valid_credentials.md b/python-test/docs/login/login_with_valid_credentials.md new file mode 100644 index 000000000..7dc352535 --- /dev/null +++ b/python-test/docs/login/login_with_valid_credentials.md @@ -0,0 +1,15 @@ +## Scenario: Login with valid credentials + +## Steps: + +1- Request authentication token using registered email referred password + +- REST API Method: POST +- endpoint: /tokens +- body: `{"email": "email", "password": "password"}` + + + +## Expected Result: + +- Status code must be 200 and a token must be returned on response \ No newline at end of file diff --git a/python-test/docs/login/request_password_with_registered_email_address.md b/python-test/docs/login/request_password_with_registered_email_address.md new file mode 100644 index 000000000..7b911eee7 --- /dev/null +++ b/python-test/docs/login/request_password_with_registered_email_address.md @@ -0,0 +1,14 @@ +## Scenario: Request password with registered email address +## Steps: + +1- On Orb auth page (`http://localhost/auth/login`) click in **"Forgot Password?"** + +2- On Orb request password page (`https://orb.live/auth/request-password`) insert a registered email on +"Email address" field + +3- Click on **"REQUEST PASSWORD"** button + +## Expected Result: + +- UI must inform that an email was sent to enable user to change the password +- User must receive an email with valid link to reset account password \ No newline at end of file diff --git a/python-test/docs/login/request_password_with_unregistered_email_address.md b/python-test/docs/login/request_password_with_unregistered_email_address.md new file mode 100644 index 000000000..4772a6d69 --- /dev/null +++ b/python-test/docs/login/request_password_with_unregistered_email_address.md @@ -0,0 +1,14 @@ +## Scenario: Request password with unregistered email address +## Steps: + +1- On Orb auth page (`http://localhost/auth/login`) click in **"Forgot Password?"** + +2- On Orb request password page (`https://orb.live/auth/request-password`) insert a unregistered email on +"Email address" field + +3- Click on **"REQUEST PASSWORD"** button + +## Expected Result: + +- UI must inform that an error has occurred +- No email must be sent diff --git a/python-test/docs/login/request_registration_of_a_registered_account_using_registered_password.md b/python-test/docs/login/request_registration_of_a_registered_account_using_registered_password.md new file mode 100644 index 000000000..d5b05e30f --- /dev/null +++ b/python-test/docs/login/request_registration_of_a_registered_account_using_registered_password.md @@ -0,0 +1,16 @@ +## Scenario: Request registration of a registered account using registered password + +## Steps: + +1 - Request an account registration using an already registered email and same registered password + +- REST API Method: POST +- endpoint: /users +- body: `{"email":"already_registered_email", "password":"registered_password"}` + +## Expected Result: + +- The request must fail with conflict (error 409), response message must be "email already taken" +- No changes should be made to the previously registered account + (name, company and password must be the ones registered for the first time) + diff --git a/python-test/docs/login/request_registration_of_a_registered_account_using_registered_password_and_company.md b/python-test/docs/login/request_registration_of_a_registered_account_using_registered_password_and_company.md new file mode 100644 index 000000000..612b5bfcd --- /dev/null +++ b/python-test/docs/login/request_registration_of_a_registered_account_using_registered_password_and_company.md @@ -0,0 +1,15 @@ +## Scenario: Request registration of a registered account using registered password and company +## Steps: + +1 - Request an account registration using an already registered email, same registered password and company field filled + +- REST API Method: POST +- endpoint: /users +- body: `{"email":"already_registered_email", "password":"registered_password", "metadata":{"company":"company"}}` + +## Expected Result: + +- The request must fail with conflict (error 409), response message must be "email already taken" +- No changes should be made to the previously registered account + (name, company and password must be the ones registered for the first time) + diff --git a/python-test/docs/login/request_registration_of_a_registered_account_using_registered_password_and_username.md b/python-test/docs/login/request_registration_of_a_registered_account_using_registered_password_and_username.md new file mode 100644 index 000000000..b1984cb9b --- /dev/null +++ b/python-test/docs/login/request_registration_of_a_registered_account_using_registered_password_and_username.md @@ -0,0 +1,16 @@ +## Scenario: Request registration of a registered account using registered password and username +## Steps: + + +1 - Request an account registration using an already registered email, same registered password and fullname field filled + +- REST API Method: POST +- endpoint: /users +- body: `{"email":"already_registered_email", "password":"registered_password", "metadata":{"fullName":"name"}}` + +## Expected Result: + +- The request must fail with conflict (error 409), response message must be "email already taken" +- No changes should be made to the previously registered account + (name, company and password must be the ones registered for the first time) + diff --git a/python-test/docs/login/request_registration_of_a_registered_account_using_registered_password_username_and_company.md b/python-test/docs/login/request_registration_of_a_registered_account_using_registered_password_username_and_company.md new file mode 100644 index 000000000..32a3e5696 --- /dev/null +++ b/python-test/docs/login/request_registration_of_a_registered_account_using_registered_password_username_and_company.md @@ -0,0 +1,15 @@ +## Scenario: Request registration of a registered account using registered password, username and company +## Steps: + +1 - Request an account registration using an already registered email, registered password and fullname and company field filled + + +- REST API Method: POST +- endpoint: /users +- body: `{"email":"already_registered_email", "password":"registered_password", "metadata":{"company":"company","fullName":"name"}}` + +## Expected Result: + +- The request must fail with conflict (error 409), response message must be "email already taken" +- No changes should be made to the previously registered account +(name, company and password must be the ones registered for the first time) diff --git a/python-test/docs/login/request_registration_of_a_registered_account_using_unregistered_password.md b/python-test/docs/login/request_registration_of_a_registered_account_using_unregistered_password.md new file mode 100644 index 000000000..f91bce6f8 --- /dev/null +++ b/python-test/docs/login/request_registration_of_a_registered_account_using_unregistered_password.md @@ -0,0 +1,14 @@ +## Scenario: Request registration of a registered account using unregistered password +## Steps: + +1 - Request an account registration using an already registered email and password different from registered + +- REST API Method: POST +- endpoint: /users +- body: `{"email":"already_registered_email", "password":"unregistered_password"}` + +## Expected Result: + +- The request must fail with conflict (error 409), response message must be "email already taken" +- No changes should be made to the previously registered account + (name, company and password must be the ones registered for the first time and the new password should not give access to the account) diff --git a/python-test/docs/login/request_registration_of_a_registered_account_using_unregistered_password_and_company.md b/python-test/docs/login/request_registration_of_a_registered_account_using_unregistered_password_and_company.md new file mode 100644 index 000000000..ba10f773c --- /dev/null +++ b/python-test/docs/login/request_registration_of_a_registered_account_using_unregistered_password_and_company.md @@ -0,0 +1,14 @@ +## Scenario: Request registration of a registered account using unregistered password and company +## Steps: + +1 - Request an account registration using an already registered email, password different from registered and company field filled + +- REST API Method: POST +- endpoint: /users +- body: `{"email":"already_registered_email", "password":"unregistered_password", "metadata":{"company":"company"}}` + +## Expected Result: + +- The request must fail with conflict (error 409), response message must be "email already taken" +- No changes should be made to the previously registered account + (name, company and password must be the ones registered for the first time and the new password should not give access to the account) \ No newline at end of file diff --git a/python-test/docs/login/request_registration_of_a_registered_account_using_unregistered_password_and_username.md b/python-test/docs/login/request_registration_of_a_registered_account_using_unregistered_password_and_username.md new file mode 100644 index 000000000..09c528c29 --- /dev/null +++ b/python-test/docs/login/request_registration_of_a_registered_account_using_unregistered_password_and_username.md @@ -0,0 +1,15 @@ +## Scenario: Request registration of a registered account using unregistered password and username +## Steps: + +1 - Request an account registration using an already registered email, password different from registered and fullname field filled + +- REST API Method: POST +- endpoint: /users +- body: `{"email":"already_registered_email", "password":"unregistered_password", "metadata":{"fullName":"name"}}` + +## Expected Result: + +- The request must fail with conflict (error 409), response message must be "email already taken" +- No changes should be made to the previously registered account + (name, company and password must be the ones registered for the first time and the new password should not give access to the account + ) diff --git a/python-test/docs/login/request_registration_of_a_registered_account_using_unregistered_password_username_and_company.md b/python-test/docs/login/request_registration_of_a_registered_account_using_unregistered_password_username_and_company.md new file mode 100644 index 000000000..460c6cf19 --- /dev/null +++ b/python-test/docs/login/request_registration_of_a_registered_account_using_unregistered_password_username_and_company.md @@ -0,0 +1,15 @@ +## Scenario: Request registration of a registered account using unregistered password, username and company +## Steps: + +1 - Request an account registration using an already registered email, unregistered password and fullname and company field filled + + +- REST API Method: POST +- endpoint: /users +- body: `{"email":"already_registered_email", "password":"unregistered_password", "metadata":{"company":"company","fullName":"name"}}` + +## Expected Result: + +- The request must fail with conflict (error 409), response message must be "email already taken" +- No changes should be made to the previously registered account + (name, company and password must be the ones registered for the first time and the new password should not give access to the account) diff --git a/python-test/docs/login/request_registration_of_an_unregistered_account_with_invalid_password_and_invalid_email.md b/python-test/docs/login/request_registration_of_an_unregistered_account_with_invalid_password_and_invalid_email.md new file mode 100644 index 000000000..c6a0147c4 --- /dev/null +++ b/python-test/docs/login/request_registration_of_an_unregistered_account_with_invalid_password_and_invalid_email.md @@ -0,0 +1,14 @@ +## Scenario: Request registration of an unregistered account with invalid password and invalid email +## Steps: + +1 - Request an account registration using an email without `@server` and password with length less than 8 + + +- REST API Method: POST +- endpoint: /users +- body: `{"email":"invalid_email", "password":"invalid_password"}` + +## Expected Result: + +- The request must fail with bad request (error 400) +- No account must be registered \ No newline at end of file diff --git a/python-test/docs/login/request_registration_of_an_unregistered_account_with_invalid_password_and_valid_email.md b/python-test/docs/login/request_registration_of_an_unregistered_account_with_invalid_password_and_valid_email.md new file mode 100644 index 000000000..a56456c3c --- /dev/null +++ b/python-test/docs/login/request_registration_of_an_unregistered_account_with_invalid_password_and_valid_email.md @@ -0,0 +1,14 @@ +## Scenario: Request registration of an unregistered account with invalid password and valid email +## Steps: + +1 - Request an account registration using a valid email and password with length less than 8 + + +- REST API Method: POST +- endpoint: /users +- body: `{"email":"email", "password":"invalid_password"}` + +## Expected Result: + +- The request must fail with bad request (error 400) and response message must be "password does not meet the requirements" +- No account must be registered \ No newline at end of file diff --git a/python-test/docs/login/request_registration_of_an_unregistered_account_with_valid_password_and_invalid_email.md b/python-test/docs/login/request_registration_of_an_unregistered_account_with_valid_password_and_invalid_email.md new file mode 100644 index 000000000..042fc12ad --- /dev/null +++ b/python-test/docs/login/request_registration_of_an_unregistered_account_with_valid_password_and_invalid_email.md @@ -0,0 +1,14 @@ +## Scenario: Request registration of an unregistered account with valid password and invalid email +## Steps: + +1 - Request an account registration using an email without `@server` and password with length greater than or equal to 8 + + +- REST API Method: POST +- endpoint: /users +- body: `{"email":"invalid_email", "password":"password"}` + +## Expected Result: + +- The request must fail with bad request (error 400) +- No account must be registered \ No newline at end of file diff --git a/python-test/docs/login/request_registration_of_an_unregistered_account_with_valid_password_and_valid_email.md b/python-test/docs/login/request_registration_of_an_unregistered_account_with_valid_password_and_valid_email.md new file mode 100644 index 000000000..5532dc71e --- /dev/null +++ b/python-test/docs/login/request_registration_of_an_unregistered_account_with_valid_password_and_valid_email.md @@ -0,0 +1,15 @@ +## Scenario: Request registration of an unregistered account with valid password and valid email +## Steps: + +1 - Request an account registration using a valid email and valid password + + +- REST API Method: POST +- endpoint: /users +- body: `{"email":"email", "password":"password"}` + +## Expected Result: + +- The request must be processed successfully (status code 201) +- The new account must be registered +- User must be able to access orb using email and password registered \ No newline at end of file diff --git a/python-test/docs/policies/check_if_is_possible_cancel_operations_with_no_change.md b/python-test/docs/policies/check_if_is_possible_cancel_operations_with_no_change.md new file mode 100644 index 000000000..dcbdec035 --- /dev/null +++ b/python-test/docs/policies/check_if_is_possible_cancel_operations_with_no_change.md @@ -0,0 +1,22 @@ +## Scenario: Check if is possible cancel operations with no change +## Steps: +1 - Create a policy + +- REST API Method: POST +- endpoint: /policies/agent/ +- header: {authorization:token} + +2 - On policies' page (`orb.live/pages/datasets/policies`) click on edit button + +3 - Change policy's name + +4 - Change policy's description and click "next" + +5 - Change policy's tap configuration options and filter and click "next" + +6 - Change policy's handler + +7 - Click "back" until return to policies' page + +## Expected Result: +- No changes must have been applied to the policy diff --git a/python-test/docs/policies/check_if_total_policies_on_policies'_page_is_correct.md b/python-test/docs/policies/check_if_total_policies_on_policies'_page_is_correct.md new file mode 100644 index 000000000..d4246fa79 --- /dev/null +++ b/python-test/docs/policies/check_if_total_policies_on_policies'_page_is_correct.md @@ -0,0 +1,20 @@ +## Scenario: Check if total policies on policies' page is correct +## Steps: +1 - Create multiple policies + +- REST API Method: POST +- endpoint: /policies/agent/ +- header: {authorization:token} + +2 - Get all existing policies + +- REST API Method: GET +- endpoint: /policies/agent/ + +3 - On policies' page (`orb.live/pages/datasets/policies`) check the total number of policies at the end of the policies table + +4 - Count the number of existing policies + +## Expected Result: +- Total policies on API response, policies page and the real number must be the same + diff --git a/python-test/docs/policies/check_policies_details.md b/python-test/docs/policies/check_policies_details.md new file mode 100644 index 000000000..3d64aa53b --- /dev/null +++ b/python-test/docs/policies/check_policies_details.md @@ -0,0 +1,15 @@ +## Scenario: Check policies details +## Steps: +1 - Create a policy + +- REST API Method: POST +- endpoint: /policies/agent/ +- header: {authorization:token} + +2 - Get a policy + +- REST API Method: GET +- endpoint: /policies/agent/ + +## Expected Result: +- Status code must be 200 and policy name, description, backend, input details and handler must be returned on response diff --git a/python-test/docs/policies/create_policy_with_description.md b/python-test/docs/policies/create_policy_with_description.md new file mode 100644 index 000000000..ec6f42b4a --- /dev/null +++ b/python-test/docs/policies/create_policy_with_description.md @@ -0,0 +1,12 @@ +## Scenario: Create policy with description +## Steps: + +1 - Create a policy with description + +- REST API Method: POST +- endpoint: /policies/agent/ +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 201 (created) and the policy must be created diff --git a/python-test/docs/policies/create_policy_with_dhcp_handler.md b/python-test/docs/policies/create_policy_with_dhcp_handler.md new file mode 100644 index 000000000..373ad59ff --- /dev/null +++ b/python-test/docs/policies/create_policy_with_dhcp_handler.md @@ -0,0 +1,12 @@ +## Scenario: Create policy with dhcp handler +## Steps: + +1 - Create a policy with dhcp handler + +- REST API Method: POST +- endpoint: /policies/agent/ +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 201 (created) and the policy must be created \ No newline at end of file diff --git a/python-test/docs/policies/create_policy_with_dns_handler.md b/python-test/docs/policies/create_policy_with_dns_handler.md new file mode 100644 index 000000000..c69134248 --- /dev/null +++ b/python-test/docs/policies/create_policy_with_dns_handler.md @@ -0,0 +1,12 @@ +## Scenario: Create policy with dns handler +## Steps: + +1 - Create a policy with dns handler + +- REST API Method: POST +- endpoint: /policies/agent/ +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 201 (created) and the policy must be created \ No newline at end of file diff --git a/python-test/docs/policies/create_policy_with_duplicate_name.md b/python-test/docs/policies/create_policy_with_duplicate_name.md new file mode 100644 index 000000000..e1599b780 --- /dev/null +++ b/python-test/docs/policies/create_policy_with_duplicate_name.md @@ -0,0 +1,13 @@ +## Scenario: Create policy with duplicate name +## Steps: +1 - Create a policy + +- REST API Method: POST +- endpoint: /policies/agent/ +- header: {authorization:token} + +2 - Create another policy using the same policy name + +## Expected Result: +- First request must have status code 201 (created) and one policy must be created on orb +- Second request must fail with status code 409 (conflict) and no other policy must be created (make sure that first policy has not been modified) diff --git a/python-test/docs/policies/create_policy_with_invalid_name_(regex).md b/python-test/docs/policies/create_policy_with_invalid_name_(regex).md new file mode 100644 index 000000000..2541247ea --- /dev/null +++ b/python-test/docs/policies/create_policy_with_invalid_name_(regex).md @@ -0,0 +1,15 @@ +## Scenario: Create policy with invalid name (regex) +## Steps: +1 - Create an policy using an invalid regex to policy name + +- REST API Method: POST +- endpoint: /policies/agent/ +- header: {authorization:token} +- example of invalid regex: + +* name starting with non-alphabetic characters +* name with just 1 letter +* space-separated composite name + +## Expected Result: +- Request must fail with status code 400 (bad request) and no policy must be created diff --git a/python-test/docs/policies/create_policy_with_multiple_handlers.md b/python-test/docs/policies/create_policy_with_multiple_handlers.md new file mode 100644 index 000000000..75380cc59 --- /dev/null +++ b/python-test/docs/policies/create_policy_with_multiple_handlers.md @@ -0,0 +1,12 @@ +## Scenario: Create policy with multiple handlers +## Steps: + +1 - Create a policy with dns, net and dhcp handler + +- REST API Method: POST +- endpoint: /policies/agent/ +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 201 (created) and the policy must be created \ No newline at end of file diff --git a/python-test/docs/policies/create_policy_with_net_handler.md b/python-test/docs/policies/create_policy_with_net_handler.md new file mode 100644 index 000000000..32f7d62d9 --- /dev/null +++ b/python-test/docs/policies/create_policy_with_net_handler.md @@ -0,0 +1,12 @@ +## Scenario: Create policy with net handler +## Steps: + +1 - Create a policy with net handler + +- REST API Method: POST +- endpoint: /policies/agent/ +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 201 (created) and the policy must be created \ No newline at end of file diff --git a/python-test/docs/policies/create_policy_with_no_agent_provisioned.md b/python-test/docs/policies/create_policy_with_no_agent_provisioned.md new file mode 100644 index 000000000..aa1c9e7e1 --- /dev/null +++ b/python-test/docs/policies/create_policy_with_no_agent_provisioned.md @@ -0,0 +1,12 @@ +## Scenario: Create policy with no agent provisioned +## Steps: +- Without provision any agent go to policies page (`https://orb.live/pages/datasets/policies`) + +1 - Click in "+ NEW POLICY" + +2- Insert a policy label +3 - Click on "NEXT' button + +## Expected Result: + +- Alert message should be displayed informing that there is no agents available \ No newline at end of file diff --git a/python-test/docs/policies/create_policy_without_description.md b/python-test/docs/policies/create_policy_without_description.md new file mode 100644 index 000000000..50caf3b0f --- /dev/null +++ b/python-test/docs/policies/create_policy_without_description.md @@ -0,0 +1,12 @@ +## Scenario: Create policy without description +## Steps: + +1 - Create a policy without description + +- REST API Method: POST +- endpoint: /policies/agent/ +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 201 (created) and the policy must be created \ No newline at end of file diff --git a/python-test/docs/policies/edit_a_policy_through_the_details_modal.md b/python-test/docs/policies/edit_a_policy_through_the_details_modal.md new file mode 100644 index 000000000..9e4149248 --- /dev/null +++ b/python-test/docs/policies/edit_a_policy_through_the_details_modal.md @@ -0,0 +1,13 @@ +## Scenario: Edit a policy through the details modal +## Steps: +1 - Create a policy + +- REST API Method: POST +- endpoint: /policies/agent/ +- header: {authorization:token} + +2 - On policies' page (`orb.live/pages/datasets/policies`) click on details button +3 - Click on "edit" button + +## Expected Result: +- User should be redirected to this policy's edit page and should be able to make changes \ No newline at end of file diff --git a/python-test/docs/policies/edit_policy_description.md b/python-test/docs/policies/edit_policy_description.md new file mode 100644 index 000000000..b898eb081 --- /dev/null +++ b/python-test/docs/policies/edit_policy_description.md @@ -0,0 +1,17 @@ +## Scenario: Edit policy description +## Steps: +1 - Create a policy + +- REST API Method: POST +- endpoint: /policies/agent/ +- header: {authorization:token} + +2- Edit this policy description + +- REST API Method: PUT +- endpoint: /policies/agent/policy_id +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 200 (ok) and changes must be applied diff --git a/python-test/docs/policies/edit_policy_handler.md b/python-test/docs/policies/edit_policy_handler.md new file mode 100644 index 000000000..1db7c73c9 --- /dev/null +++ b/python-test/docs/policies/edit_policy_handler.md @@ -0,0 +1,18 @@ +## Scenario: Edit policy handler +## Step: + +1 - Create a policy + +- REST API Method: POST +- endpoint: /policies/agent/ +- header: {authorization:token} + +2- Edit this policy handler + +- REST API Method: PUT +- endpoint: /policies/agent/policy_id +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 200 (ok) and changes must be applied diff --git a/python-test/docs/policies/edit_policy_name.md b/python-test/docs/policies/edit_policy_name.md new file mode 100644 index 000000000..a4a575b2b --- /dev/null +++ b/python-test/docs/policies/edit_policy_name.md @@ -0,0 +1,17 @@ +## Scenario: Edit policy name +## Steps: +1 - Create a policy + +- REST API Method: POST +- endpoint: /policies/agent/ +- header: {authorization:token} + +2- Edit this policy name + +- REST API Method: PUT +- endpoint: /policies/agent/policy_id +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 200 (ok) and changes must be applied \ No newline at end of file diff --git a/python-test/docs/policies/remove_policy_using_correct_name.md b/python-test/docs/policies/remove_policy_using_correct_name.md new file mode 100644 index 000000000..b37d15162 --- /dev/null +++ b/python-test/docs/policies/remove_policy_using_correct_name.md @@ -0,0 +1,16 @@ +## Scenario: Remove policy using correct name +## Steps: +1 - Create a policy + +- REST API Method: POST +- endpoint: /policies/agent/ +- header: {authorization:token} + +2 - On policies' page (`orb.live/pages/datasets/policies`) click on remove button +3 - Insert the name of the policy correctly on delete modal +4 - Confirm the operation by clicking on "I UNDERSTAND, DELETE THIS POLICY" button + +## Expected Result: +- Policy must be deleted + + \ No newline at end of file diff --git a/python-test/docs/policies/remove_policy_using_incorrect_name.md b/python-test/docs/policies/remove_policy_using_incorrect_name.md new file mode 100644 index 000000000..1a4337140 --- /dev/null +++ b/python-test/docs/policies/remove_policy_using_incorrect_name.md @@ -0,0 +1,14 @@ +## Scenario: Remove policy using incorrect name +## Steps: +1 - Create a policy + +- REST API Method: POST +- endpoint: /policies/agent/ +- header: {authorization:token} + +2 - On policies' page (`orb.live/pages/datasets/policies`) click on remove button +3 - Insert the name of the policy incorrectly on delete modal + +## Expected Result: +- "I UNDERSTAND, DELETE THIS POLICY" button must not be enabled +- After user close the deletion modal, policy must not be deleted diff --git a/python-test/docs/policies/test_policy_filters.md b/python-test/docs/policies/test_policy_filters.md new file mode 100644 index 000000000..abc661740 --- /dev/null +++ b/python-test/docs/policies/test_policy_filters.md @@ -0,0 +1,19 @@ +## Scenario: Test policy filters +## Steps: +1 - Create multiple policies + +- REST API Method: POST +- endpoint: /policies/agent/ +- header: {authorization:token} + +2 - On policies' page (`orb.live/pages/datasets/policies`) use the filter: + + * Name + * Description + * Version + * Search by + + +## Expected Result: + +- All filters must be working properly diff --git a/python-test/docs/sanity.md b/python-test/docs/sanity.md new file mode 100644 index 000000000..b5ecf056a --- /dev/null +++ b/python-test/docs/sanity.md @@ -0,0 +1,133 @@ +## Sanity tests +## Login + +- [Request registration of a registered account using registered password username and company](login/request_registration_of_a_registered_account_using_registered_password_username_and_company.md) +- [Request registration of a registered account using registered password and username](login/request_registration_of_a_registered_account_using_registered_password_and_username.md) +- [Request registration of a registered account using registered password and company](login/request_registration_of_a_registered_account_using_registered_password_and_company.md) +- [Request registration of a registered account using registered password](login/request_registration_of_a_registered_account_using_registered_password.md) +- [Request registration of a registered account using unregistered password username and company](login/request_registration_of_a_registered_account_using_unregistered_password_username_and_company.md) +- [Request registration of a registered account using unregistered password and username](login/request_registration_of_a_registered_account_using_unregistered_password_and_username.md) +- [Request registration of a registered account using unregistered password and company](login/request_registration_of_a_registered_account_using_unregistered_password_and_company.md) +- [Request registration of a registered account using unregistered password](login/request_registration_of_a_registered_account_using_unregistered_password.md) +- [Request registration of an unregistered account with valid password and invalid email](login/request_registration_of_an_unregistered_account_with_valid_password_and_invalid_email.md) +- [Request registration of an unregistered account with valid password and valid email](login/request_registration_of_an_unregistered_account_with_valid_password_and_valid_email.md) +- [Request registration of an unregistered account with invalid password and valid email](login/request_registration_of_an_unregistered_account_with_invalid_password_and_valid_email.md) +- [Request registration of an unregistered account with invalid password and invalid email](login/request_registration_of_an_unregistered_account_with_invalid_password_and_invalid_email.md) +- [Check if email and password are required fields](login/check_if_email_and_password_are_required_fields.md) +- [Login with valid credentials](login/login_with_valid_credentials.md) +- [Login with invalid credentials](login/login_with_invalid_credentials.md) +- [Request password with registered email address](login/request_password_with_registered_email_address.md) +- [Request password with unregistered email address](login/request_password_with_unregistered_email_address.md) + + +## Agents + +- [Create agent without tags](agents/create_agent_without_tags.md) +- [Create agent with one tag](agents/create_agent_with_one_tag.md) +- [Create agent with multiple tags](agents/create_agent_with_multiple_tags.md) +- [Create agent with invalid name (regex)](agents/create_agent_with_invalid_name_(regex).md) +- [Create agent with duplicate name](agents/create_agent_with_duplicate_name.md) +- [Check agent details](agents/check_agent_details.md) +- [Edit an agent through the details modal](agents/edit_an_agent_through_the_details_modal.md) +- [Edit agent name](agents/edit_agent_name.md) +- [Edit agent tag](agents/edit_agent_tag.md) +- [Save agent without tag](agents/save_agent_without_tag.md) +- [Insert tags in agents created without tags](agents/insert_tags_in_agents_created_without_tags.md) +- [Remove agent using correct name](agents/remove_agent_using_correct_name.md) +- [Remove agent using incorrect name](agents/remove_agent_using_incorrect_name.md) +- [Run two orb agents on the same port](agents/run_two_orb_agents_on_the_same_port.md) +- [Run two orb agents on different ports](agents/run_two_orb_agents_on_different_ports.md) +- [Edit agent name and tag](agents/edit_agent_name_and_tags.md) + + +## Agent Groups + +- [Create agent group with invalid name (regex)](agent_groups/create_agent_group_with_invalid_name_(regex).md) +- [Create agent group with duplicate name](agent_groups/create_agent_group_with_duplicate_name.md) +- [Create agent group with description](agent_groups/create_agent_group_with_description.md) +- [Create agent group without description](agent_groups/create_agent_group_without_description.md) +- [Create agent group without tag](agent_groups/create_agent_group_without_tag.md) +- [Create agent group with one tag](agent_groups/create_agent_group_with_one_tag.md) +- [Create agent group with multiple tags](agent_groups/create_agent_group_with_multiple_tags.md) +- [Visualize matching agents](agent_groups/visualize_matching_agents.md) +- [Check agent groups details](agent_groups/check_agent_groups_details.md) +- [Edit an agent group through the details modal](agent_groups/edit_an_agent_group_through_the_details_modal.md) +- [Edit agent group name](agent_groups/edit_agent_group_name.md) +- [Edit agent group description](agent_groups/edit_agent_group_description.md) +- [Edit agent group tag](agent_groups/edit_agent_group_tag.md) +- [Remove agent group using correct name](agent_groups/remove_agent_group_using_correct_name.md) +- [Remove agent group using incorrect name](agent_groups/remove_agent_group_using_incorrect_name.md) + + +## Sinks + +- [Create sink with invalid name (regex)](sinks/create_sink_with_invalid_name_(regex).md) +- [Create sink with duplicate name](sinks/create_sink_with_duplicate_name.md) +- [Create sink with description](sinks/create_sink_with_description.md) +- [Create sink without description](sinks/create_sink_without_description.md) +- [Create sink without tags](sinks/create_sink_without_tags.md) +- [Check if remote host, username and password are required to create a sink](sinks/check_if_remote_host,_username_and_password_are_required_to_create_a_sink.md) +- [Check sink details](sinks/check_sink_details.md) +- [Edit a sink through the details modal](sinks/edit_a_sink_through_the_details_modal.md) +- [Edit sink name](sinks/edit_sink_name.md) +- [Edit sink description](sinks/edit_sink_description.md) +- [Edit sink remote host](sinks/edit_sink_remote_host.md) +- [Edit sink username](sinks/edit_sink_username.md) +- [Edit sink password](sinks/edit_sink_password.md) +- [Edit sink tags](sinks/edit_sink_tags.md) +- [Remove sink using correct name](sinks/remove_sink_using_correct_name.md) +- [Remove sink using incorrect name](sinks/remove_sink_using_incorrect_name.md) + +## Policies + +- [Create policy with invalid name (regex)](policies/create_policy_with_invalid_name_(regex).md) +- [Create policy with no agent provisioned](policies/create_policy_with_no_agent_provisioned.md) +- [Create policy with duplicate name](policies/create_policy_with_duplicate_name.md) +- [Create policy with description](policies/create_policy_with_description.md) +- [Create policy without description](policies/create_policy_without_description.md) +- [Create policy with dhcp handler](policies/create_policy_with_dhcp_handler.md) +- [Create policy with dns handler](policies/create_policy_with_dns_handler.md) +- [Create policy with net handler](policies/create_policy_with_net_handler.md) +- [Create policy with multiple handlers](policies/create_policy_with_multiple_handlers.md) +- [Check policies details](policies/check_policies_details.md) +- [Edit a policy through the details modal](policies/edit_a_policy_through_the_details_modal.md) +- [Edit policy name](policies/edit_policy_name.md) +- [Edit policy description](policies/edit_policy_description.md) +- [Edit policy handler](policies/edit_policy_handler.md) +- [Remove policy using correct name](policies/remove_policy_using_correct_name.md) +- [Remove policy using incorrect name](policies/remove_policy_using_incorrect_name.md) + +## Datasets + +- [Create dataset with invalid name (regex)](datasets/create_dataset_with_invalid_name_(regex).md) +- [Create dataset](datasets/create_dataset.md) +- [Check datasets details](datasets/check_datasets_details.md) +- [Edit dataset name](datasets/edit_dataset_name.md) +- [Edit dataset sink](datasets/edit_dataset_sink.md) +- [Remove dataset using correct name](datasets/remove_dataset_using_correct_name.md) +- [Remove dataset using incorrect name](datasets/remove_dataset_using_incorrect_name.md) + +## Integration tests + +- [Check if sink is active while scraping metrics](integration/sink_active_while_scraping_metrics.md) +- [Check if sink with invalid credentials becomes active](integration/sink_error_invalid_credentials.md) +- [Provision agent before group (check if agent subscribes to the group)](integration/provision_agent_before_group.md) +- [Provision agent after group (check if agent subscribes to the group)](integration/provision_agent_after_group.md) +- [Provision agent with tag matching existing group linked to a valid dataset](integration/multiple_agents_subscribed_to_a_group.md) +- [Apply multiple policies to a group](integration/apply_multiple_policies.md) +- [Apply multiple policies to a group and remove one policy](integration/remove_one_of_multiple_policies.md) +- [Apply multiple policies to a group and remove one dataset](integration/remove_one_of_multiple_datasets.md) +- [Apply the same policy twice to the agent](integration/apply_policy_twice.md) +- [Remove group (invalid dataset, agent logs)](integration/remove_group.md) +- [Remove sink (invalid dataset, agent logs)](integration/remove_sink.md) +- [Remove policy (invalid dataset, agent logs, heartbeat)](integration/remove_policy.md) +- [Remove dataset (check agent logs, heartbeat)](integration/remove_dataset.md) +- [Remove agent container (logs, agent groups matches)](integration/remove_agent_container.md) +- [Remove agent container force (logs, agent groups matches)](integration/remove_agent_container_force.md) +- [Remove agent (logs, agent groups matches)](integration/remove_agent.md) +- [Subscribe an agent to multiple groups created before agent provisioning](integration/subscribe_an_agent_to_multiple_groups_created_before_agent_provisioning.md) +- [Subscribe an agent to multiple groups created after agent provisioning](integration/subscribe_an_agent_to_multiple_groups_created_after_agent_provisioning.md) +- [Agent subscription to group after editing agent's tags](integration/agent_subscription_to_group_after_editing_agent's_tags.md) +- [Agent subscription to group with policies after editing agent's tags](integration/agent_subscription_to_group_with_policies_after_editing_agent's_tags.md) +- [Edit agent name and apply policies to then](integration/edit_agent_name_and_apply_policies_to_then.md) +- [Insert tags in agents created without tags and apply policies to group matching new tags.md](integration/insert_tags_in_agents_created_without_tags_and_apply_policies_to_group_matching_new_tags.md) diff --git a/python-test/docs/sinks/check_if_is_possible_cancel_operations_with_no_change.md b/python-test/docs/sinks/check_if_is_possible_cancel_operations_with_no_change.md new file mode 100644 index 000000000..3ed43df35 --- /dev/null +++ b/python-test/docs/sinks/check_if_is_possible_cancel_operations_with_no_change.md @@ -0,0 +1,24 @@ +## Scenario: Check if is possible cancel operations with no change +## Steps: +1 - Create a sink + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} + +2 - On sinks' page (`orb.live/pages/sinks`) click on edit button + +3 - Change sinks' name + +4 - Change sink's description and click "next" + +5 - Change sink's remote host + +6 - Change sink's username + +7 - Change sink's password + +8 - Click "back" until return to sinks' page + +## Expected Result: +- No changes must have been applied to the sink diff --git a/python-test/docs/sinks/check_if_remote_host,_username_and_password_are_required_to_create_a_sink.md b/python-test/docs/sinks/check_if_remote_host,_username_and_password_are_required_to_create_a_sink.md new file mode 100644 index 000000000..f8d5c3e9b --- /dev/null +++ b/python-test/docs/sinks/check_if_remote_host,_username_and_password_are_required_to_create_a_sink.md @@ -0,0 +1,49 @@ +## Scenario: Check if remote host, username and password are required to create a sink + +--------------------------------------------------------- + +## Without remote host + + +## Steps: +1 - Create a sink without remote host + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} + +## Expected Result: + +- Request must fail with status code 400 (bad request) + +-------------------------------------------------------- + +## Without username + + +## Steps: +1 - Create a sink without username + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} + +## Expected Result: + +- Request must fail with status code 400 (bad request) + +-------------------------------------------------------- + +## Without password + + +## Steps: +1 - Create a sink without password + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} + +## Expected Result: + +- Request must fail with status code 400 (bad request) \ No newline at end of file diff --git a/python-test/docs/sinks/check_if_total_sinks_on_sinks'_page_is_correct.md b/python-test/docs/sinks/check_if_total_sinks_on_sinks'_page_is_correct.md new file mode 100644 index 000000000..e3753e751 --- /dev/null +++ b/python-test/docs/sinks/check_if_total_sinks_on_sinks'_page_is_correct.md @@ -0,0 +1,21 @@ +## Scenario: Check if total sinks on sinks' page is correct +## Steps: +1 - Create multiple sinks + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} + +2 - Get all existing sinks + +- REST API Method: GET +- endpoint: /sinks + +3 - On sinks' page (`orb.live/pages/sinks`) check the total number of sinks at the end of the sinks table + +4 - Count the number of existing sinks + +## Expected Result: +- Total sinks on API response, sinks page and the real number must be the same + + diff --git a/python-test/docs/sinks/check_sink_details.md b/python-test/docs/sinks/check_sink_details.md new file mode 100644 index 000000000..4cb75d8f3 --- /dev/null +++ b/python-test/docs/sinks/check_sink_details.md @@ -0,0 +1,19 @@ +## Scenario: Check sink details +## Steps: +1 - Create a sink + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} + +2 - Get a sink + +- REST API Method: GET +- endpoint: /sinks/sink_id + +## Expected Result: +- Status code must be 200 and sink name, description, service type, remote host, status, username and tags must be returned on response + + * If a sink never received data, status must be `new` + * If a sink is receiving data, status must be `active` + * If a sink has not received data for more than 30 minutes, status must be `idle` \ No newline at end of file diff --git a/python-test/docs/sinks/create_sink_with_description.md b/python-test/docs/sinks/create_sink_with_description.md new file mode 100644 index 000000000..c09153b7e --- /dev/null +++ b/python-test/docs/sinks/create_sink_with_description.md @@ -0,0 +1,12 @@ +## Scenario: Create sink with description +## Steps: + +1 - Create a sink with description + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 201 (created) and the sink must be created diff --git a/python-test/docs/sinks/create_sink_with_duplicate_name.md b/python-test/docs/sinks/create_sink_with_duplicate_name.md new file mode 100644 index 000000000..134a49c9e --- /dev/null +++ b/python-test/docs/sinks/create_sink_with_duplicate_name.md @@ -0,0 +1,14 @@ +## Scenario: Create sink with duplicate name +## Steps: +1 - Create a sink + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} + +2 - Create another sink using the same sink name + +## Expected Result: +- First request must have status code 201 (created) and one sink must be created on orb +- Second request must fail with status code 409 (conflict) and no other sink must be created (make sure that first sink has not been modified) + diff --git a/python-test/docs/sinks/create_sink_with_invalid_name_(regex).md b/python-test/docs/sinks/create_sink_with_invalid_name_(regex).md new file mode 100644 index 000000000..dc095ece9 --- /dev/null +++ b/python-test/docs/sinks/create_sink_with_invalid_name_(regex).md @@ -0,0 +1,14 @@ +## Scenario: Create sink with invalid name (regex) +## Steps: +1 - Create a sink using an invalid regex to sink name + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} +- example of invalid regex: + * name starting with non-alphabetic characters + * name with just 1 letter + * space-separated composite name + +## Expected Result: +- Request must fail with status code 400 (bad request) and no sink must be created diff --git a/python-test/docs/sinks/create_sink_with_multiple_tags.md b/python-test/docs/sinks/create_sink_with_multiple_tags.md new file mode 100644 index 000000000..2a935bc3a --- /dev/null +++ b/python-test/docs/sinks/create_sink_with_multiple_tags.md @@ -0,0 +1,12 @@ +## Scenario: Create sink with multiple tags +## Steps: +1 - Create a sink with more than one pair (key:value) of tags + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 201 (created) and the sink must be created +- Tags for sink just serve to filter the sinks \ No newline at end of file diff --git a/python-test/docs/sinks/create_sink_with_tags.md b/python-test/docs/sinks/create_sink_with_tags.md new file mode 100644 index 000000000..248ab56ff --- /dev/null +++ b/python-test/docs/sinks/create_sink_with_tags.md @@ -0,0 +1,12 @@ +## Scenario: Create sink with tags +## Steps: +1 - Create a sink with one pair (key:value) of tags + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 201 (created) and the sink must be created +- Tags for sink just serve to filter the sinks diff --git a/python-test/docs/sinks/create_sink_without_description.md b/python-test/docs/sinks/create_sink_without_description.md new file mode 100644 index 000000000..d8936d036 --- /dev/null +++ b/python-test/docs/sinks/create_sink_without_description.md @@ -0,0 +1,12 @@ +## Scenario: Create sink without description +## Steps: +1 - Create a sink without description + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 201 (created) and the sink must be created +- Tags for sink just serve to filter the sinks \ No newline at end of file diff --git a/python-test/docs/sinks/create_sink_without_tags.md b/python-test/docs/sinks/create_sink_without_tags.md new file mode 100644 index 000000000..c1694dbe0 --- /dev/null +++ b/python-test/docs/sinks/create_sink_without_tags.md @@ -0,0 +1,12 @@ +## Scenario: Create sink without tags +## Steps: +1 - Create a sink without any pair (key:value) of tags + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 201 (created) and the sink must be created +- Tags for sink just serve to filter the sinks \ No newline at end of file diff --git a/python-test/docs/sinks/edit_a_sink_through_the_details_modal.md b/python-test/docs/sinks/edit_a_sink_through_the_details_modal.md new file mode 100644 index 000000000..4f75cb8b9 --- /dev/null +++ b/python-test/docs/sinks/edit_a_sink_through_the_details_modal.md @@ -0,0 +1,13 @@ +## Scenario: Edit a sink through the details modal +## Steps: +1 - Create a sink + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} + +2 - On sinks' page (`orb.live/pages/sinks`) click on details button +3 - Click on "edit" button + +## Expected Result: +- User should be redirected to this sink's edit page and should be able to make changes \ No newline at end of file diff --git a/python-test/docs/sinks/edit_sink_description.md b/python-test/docs/sinks/edit_sink_description.md new file mode 100644 index 000000000..48f6c56dc --- /dev/null +++ b/python-test/docs/sinks/edit_sink_description.md @@ -0,0 +1,19 @@ +## Scenario: Edit sink description +## Steps: +1 - Create a sink + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} + +2- Edit this sink description + +- REST API Method: PUT +- endpoint: /sinks/sink_id +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 200 (ok) and changes must be applied + + diff --git a/python-test/docs/sinks/edit_sink_name.md b/python-test/docs/sinks/edit_sink_name.md new file mode 100644 index 000000000..dcca3fba5 --- /dev/null +++ b/python-test/docs/sinks/edit_sink_name.md @@ -0,0 +1,18 @@ +## Scenario: Edit sink name +## Steps: +1 - Create a sink + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} + +2- Edit this sink name + +- REST API Method: PUT +- endpoint: /sinks/sink_id +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 200 (ok) and changes must be applied + \ No newline at end of file diff --git a/python-test/docs/sinks/edit_sink_password.md b/python-test/docs/sinks/edit_sink_password.md new file mode 100644 index 000000000..051514a58 --- /dev/null +++ b/python-test/docs/sinks/edit_sink_password.md @@ -0,0 +1,18 @@ +## Scenario: Edit sink password +## Steps: +1 - Create a sink + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} + +2- Edit this sink password + +- REST API Method: PUT +- endpoint: /sinks/sink_id +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 200 (ok) and changes must be applied + \ No newline at end of file diff --git a/python-test/docs/sinks/edit_sink_remote_host.md b/python-test/docs/sinks/edit_sink_remote_host.md new file mode 100644 index 000000000..6d3b20433 --- /dev/null +++ b/python-test/docs/sinks/edit_sink_remote_host.md @@ -0,0 +1,18 @@ +## Scenario: Edit sink remote host +## Steps: +1 - Create a sink + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} + +2- Edit this sink remote host + +- REST API Method: PUT +- endpoint: /sinks/sink_id +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 200 (ok) and changes must be applied + \ No newline at end of file diff --git a/python-test/docs/sinks/edit_sink_tags.md b/python-test/docs/sinks/edit_sink_tags.md new file mode 100644 index 000000000..fc0566f7d --- /dev/null +++ b/python-test/docs/sinks/edit_sink_tags.md @@ -0,0 +1,18 @@ +## Scenario: Edit sink tags +## Steps: +1 - Create a sink + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} + +2- Edit this sink tags + +- REST API Method: PUT +- endpoint: /sinks/sink_id +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 200 (ok) and changes must be applied + \ No newline at end of file diff --git a/python-test/docs/sinks/edit_sink_username.md b/python-test/docs/sinks/edit_sink_username.md new file mode 100644 index 000000000..3a211dcf1 --- /dev/null +++ b/python-test/docs/sinks/edit_sink_username.md @@ -0,0 +1,18 @@ +## Scenario: Edit sink username +## Steps: +1 - Create a sink + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} + +2- Edit this sink username + +- REST API Method: PUT +- endpoint: /sinks/sink_id +- header: {authorization:token} + + +## Expected Result: +- Request must have status code 200 (ok) and changes must be applied + \ No newline at end of file diff --git a/python-test/docs/sinks/remove_sink_using_correct_name.md b/python-test/docs/sinks/remove_sink_using_correct_name.md new file mode 100644 index 000000000..644417022 --- /dev/null +++ b/python-test/docs/sinks/remove_sink_using_correct_name.md @@ -0,0 +1,15 @@ +## Scenario: Remove sink using correct name +## Steps: +1 - Create a sink + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} + +2 - On agent groups' page (`orb.live/pages/sinks`) click on remove button +3 - Insert the name of the sink correctly on delete modal +4 - Confirm the operation by clicking on "I UNDERSTAND, DELETE THIS SINK" button + +## Expected Result: +- Sink must be deleted + diff --git a/python-test/docs/sinks/remove_sink_using_incorrect_name.md b/python-test/docs/sinks/remove_sink_using_incorrect_name.md new file mode 100644 index 000000000..c1ff8dc5f --- /dev/null +++ b/python-test/docs/sinks/remove_sink_using_incorrect_name.md @@ -0,0 +1,15 @@ +## Scenario: Remove sink using incorrect name +## Steps: +1 - Create a sink + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} + +2 - On agent groups' page (`orb.live/pages/sinks`) click on remove button +3 - Insert the name of the sink correctly on delete modal + +## Expected Result: +- Sink must be deleted +- "I UNDERSTAND, DELETE THIS SINK" button must not be enabled +- After user close the deletion modal, sink must not be deleted \ No newline at end of file diff --git a/python-test/docs/sinks/test_sink_filters.md b/python-test/docs/sinks/test_sink_filters.md new file mode 100644 index 000000000..8a406e8d5 --- /dev/null +++ b/python-test/docs/sinks/test_sink_filters.md @@ -0,0 +1,23 @@ +## Scenario: Test sink filters +## Steps: +1 - Create multiple sinks + +- REST API Method: POST +- endpoint: /sinks +- header: {authorization:token} + +2 - On sinks' page (`orb.live/pages/sinks`) use the filter: + + * Name + * Description + * Type + * Status + * Tags + * Search by + + +## Expected Result: + +- All filters must be working properly + + diff --git a/python-test/docs/smoke.md b/python-test/docs/smoke.md new file mode 100644 index 000000000..8a6045140 --- /dev/null +++ b/python-test/docs/smoke.md @@ -0,0 +1,88 @@ +## Smoke tests + +## Login + +- [Request registration of a registered account using registered password username and company](login/request_registration_of_a_registered_account_using_registered_password_username_and_company.md) +- [Request registration of a registered account using registered password and username](login/request_registration_of_a_registered_account_using_registered_password_and_username.md) +- [Request registration of a registered account using registered password and company](login/request_registration_of_a_registered_account_using_registered_password_and_company.md) +- [Request registration of a registered account using registered password](login/request_registration_of_a_registered_account_using_registered_password.md) +- [Request registration of a registered account using unregistered password username and company](login/request_registration_of_a_registered_account_using_unregistered_password_username_and_company.md) +- [Request registration of a registered account using unregistered password and username](login/request_registration_of_a_registered_account_using_unregistered_password_and_username.md) +- [Request registration of a registered account using unregistered password and company](login/request_registration_of_a_registered_account_using_unregistered_password_and_company.md) +- [Request registration of a registered account using unregistered password](login/request_registration_of_a_registered_account_using_unregistered_password.md) +- [Request registration of an unregistered account with valid password and invalid email](login/request_registration_of_an_unregistered_account_with_valid_password_and_invalid_email.md) +- [Request registration of an unregistered account with valid password and valid email](login/request_registration_of_an_unregistered_account_with_valid_password_and_valid_email.md) +- [Request registration of an unregistered account with invalid password and valid email](login/request_registration_of_an_unregistered_account_with_invalid_password_and_valid_email.md) +- [Request registration of an unregistered account with invalid password and invalid email](login/request_registration_of_an_unregistered_account_with_invalid_password_and_invalid_email.md) +- [Check if email and password are required fields](login/check_if_email_and_password_are_required_fields.md) +- [Login with valid credentials](login/login_with_valid_credentials.md) +- [Login with invalid credentials](login/login_with_invalid_credentials.md) +- [Request password with registered email address](login/request_password_with_registered_email_address.md) + + +## Agents + +- [Create agent with one tag](agents/create_agent_with_one_tag.md) +- [Edit agent name](agents/edit_agent_name.md) +- [Edit agent tag](agents/edit_agent_tag.md) +- [Save agent without tag](agents/save_agent_without_tag.md) +- [Insert tags in agents created without tags](agents/insert_tags_in_agents_created_without_tags.md) +- [Remove agent using correct name](agents/remove_agent_using_correct_name.md) +- [Run two orb agents on the same port](agents/run_two_orb_agents_on_the_same_port.md) +- [Run two orb agents on different ports](agents/run_two_orb_agents_on_different_ports.md) + +## Agent Groups + +- [Create agent group with description](agent_groups/create_agent_group_with_description.md) +- [Create agent group with one tag](agent_groups/create_agent_group_with_one_tag.md) +- [Edit agent group name](agent_groups/edit_agent_group_name.md) +- [Edit agent group tag](agent_groups/edit_agent_group_tag.md) +- [Remove agent group using correct name](agent_groups/remove_agent_group_using_correct_name.md) + + +## Sinks + +- [Create sink with description](sinks/create_sink_with_description.md) +- [Create sink without tags](sinks/create_sink_without_tags.md) +- [Remove sink using correct name](sinks/remove_sink_using_correct_name.md) + +## Policies + +- [Create policy with description](policies/create_policy_with_description.md) +- [Create policy with dhcp handler](policies/create_policy_with_dhcp_handler.md) +- [Create policy with dns handler](policies/create_policy_with_dns_handler.md) +- [Create policy with net handler](policies/create_policy_with_net_handler.md) +- [Edit policy handler](policies/edit_policy_handler.md) +- [Remove policy using correct name](policies/remove_policy_using_correct_name.md) + + +## Datasets + +- [Create dataset](datasets/create_dataset.md) +- [Remove dataset using correct name](datasets/remove_dataset_using_correct_name.md) + + +## Integration tests + +- [Check if sink is active while scraping metrics](integration/sink_active_while_scraping_metrics.md) +- [Check if sink with invalid credentials becomes active](integration/sink_error_invalid_credentials.md) +- [Provision agent before group (check if agent subscribes to the group)](integration/provision_agent_before_group.md) +- [Provision agent after group (check if agent subscribes to the group)](integration/provision_agent_after_group.md) +- [Provision agent with tag matching existing group linked to a valid dataset](integration/multiple_agents_subscribed_to_a_group.md) +- [Apply multiple policies to a group](integration/apply_multiple_policies.md) +- [Apply multiple policies to a group and remove one policy](integration/remove_one_of_multiple_policies.md) +- [Apply multiple policies to a group and remove one dataset](integration/remove_one_of_multiple_datasets.md) +- [Apply the same policy twice to the agent](integration/apply_policy_twice.md) +- [Remove group (invalid dataset, agent logs)](integration/remove_group.md) +- [Remove sink (invalid dataset, agent logs)](integration/remove_sink.md) +- [Remove policy (invalid dataset, agent logs, heartbeat)](integration/remove_policy.md) +- [Remove dataset (check agent logs, heartbeat)](integration/remove_dataset.md) +- [Remove agent container (logs, agent groups matches)](integration/remove_agent_container.md) +- [Remove agent container force (logs, agent groups matches)](integration/remove_agent_container_force.md) +- [Remove agent (logs, agent groups matches)](integration/remove_agent.md) +- [Subscribe an agent to multiple groups created before agent provisioning](integration/subscribe_an_agent_to_multiple_groups_created_before_agent_provisioning.md) +- [Subscribe an agent to multiple groups created after agent provisioning](integration/subscribe_an_agent_to_multiple_groups_created_after_agent_provisioning.md) +- [Agent subscription to group after editing agent's tags](integration/agent_subscription_to_group_after_editing_agent's_tags.md) +- [Agent subscription to group with policies after editing agent's tags](integration/agent_subscription_to_group_with_policies_after_editing_agent's_tags.md) +- [Edit agent name and apply policies to then](integration/edit_agent_name_and_apply_policies_to_then.md) +- [Insert tags in agents created without tags and apply policies to group matching new tags.md](integration/insert_tags_in_agents_created_without_tags_and_apply_policies_to_group_matching_new_tags.md) diff --git a/python-test/features/agentGroups.feature b/python-test/features/agentGroups.feature index 8128d312b..87649d249 100644 --- a/python-test/features/agentGroups.feature +++ b/python-test/features/agentGroups.feature @@ -1,10 +1,118 @@ @agentGroups Feature: agent groups creation - Scenario: Create Agent Group + Scenario: Create Agent Group with one tag Given the Orb user has a registered account And the Orb user logs in - And that an agent already exists and is online + And that an agent with 1 orb tag(s) already exists and is online When an Agent Group is created with same tag as the agent - Then one agent must be matching on response field matching_agents + Then 1 agent must be matching on response field matching_agents And the container logs should contain the message "completed RPC subscription to group" within 10 seconds + + + Scenario: Create Agent Group with multiple tags + Given the Orb user has a registered account + And the Orb user logs in + And that an agent with 5 orb tag(s) already exists and is online + When an Agent Group is created with same tag as the agent + Then 1 agent must be matching on response field matching_agents + And the container logs should contain the message "completed RPC subscription to group" within 10 seconds + + + Scenario: Create Agent Group without tags + Given the Orb user has a registered account + And the Orb user logs in + When an Agent Group is created with 0 orb tag(s) + Then Agent Group creation response must be an error with message 'malformed entity specification' + + + Scenario: Create Agent Group without description + Given the Orb user has a registered account + And the Orb user logs in + And that an agent with region:br orb tag(s) already exists and is online + When an Agent Group is created with region:br orb tag(s) and without description + Then 1 agent must be matching on response field matching_agents + And the container logs should contain the message "completed RPC subscription to group" within 10 seconds + + + Scenario: Edit Agent Group name + Given the Orb user has a registered account + And the Orb user logs in + And that an agent with region:br orb tag(s) already exists and is online + And an Agent Group is created with same tag as the agent + When the name of Agent Group is edited using: name=group_name + Then 1 agent must be matching on response field matching_agents + And the container logs should contain the message "completed RPC subscription to group" within 10 seconds + + + Scenario: Agent Group name editing without name + Given the Orb user has a registered account + And the Orb user logs in + And that an agent with region:br orb tag(s) already exists and is online + And an Agent Group is created with same tag as the agent + When the name of Agent Group is edited using: name=None + Then agent group editing must fail + And 1 agent must be matching on response field matching_agents + And the container logs should contain the message "completed RPC subscription to group" within 10 seconds + + + Scenario: Edit Agent Group description (without description) + Given the Orb user has a registered account + And the Orb user logs in + And that an agent with region:br orb tag(s) already exists and is online + And an Agent Group is created with same tag as the agent + When the description of Agent Group is edited using: description=None + Then 1 agent must be matching on response field matching_agents + And the container logs should contain the message "completed RPC subscription to group" within 10 seconds + + + Scenario: Edit Agent Group description (with description) + Given the Orb user has a registered account + And the Orb user logs in + And that an agent with region:br orb tag(s) already exists and is online + And an Agent Group is created with same tag as the agent + When the description of Agent Group is edited using: description="Agent group test description" + Then 1 agent must be matching on response field matching_agents + And the container logs should contain the message "completed RPC subscription to group" within 10 seconds + + + Scenario: Edit Agent Group tags (with tags - unsubscription) + Given the Orb user has a registered account + And the Orb user logs in + And that an agent with region:br orb tag(s) already exists and is online + And an Agent Group is created with same tag as the agent + When the tags of Agent Group is edited using: tags=another:tag, region:br + Then 0 agent must be matching on response field matching_agents + And the container logs should contain the message "completed RPC unsubscription to group" within 10 seconds + + + Scenario: Edit Agent Group tags (with tags - subscription) + Given the Orb user has a registered account + And the Orb user logs in + And that an agent with ns1:true orb tag(s) already exists and is online + And an Agent Group is created with another:tag orb tag(s) + When the tags of Agent Group is edited using: tags=ns1:true + Then 1 agent must be matching on response field matching_agents + And the container logs contain the message "completed RPC subscription to group" referred to each matching group within 10 seconds + + + Scenario: Edit Agent Group tags (without tags) + Given the Orb user has a registered account + And the Orb user logs in + And that an agent with region:br orb tag(s) already exists and is online + And an Agent Group is created with same tag as the agent + When the tags of Agent Group is edited using: tags=None + Then agent group editing must fail + And 1 agent must be matching on response field matching_agents + And the agent status in Orb should be online + + + Scenario: Edit Agent Group name, description and tags + Given the Orb user has a registered account + And the Orb user logs in + And that an agent with region:br orb tag(s) already exists and is online + And an Agent Group is created with same tag as the agent + When the name, tags, description of Agent Group is edited using: name=new_name/ tags=region:br, ns1:true/ description=None + Then the container logs should contain the message "completed RPC unsubscription to group" within 10 seconds + And 0 agent must be matching on response field matching_agents + And the agent status in Orb should be online \ No newline at end of file diff --git a/python-test/features/agentProviderUi.feature b/python-test/features/agentProviderUi.feature index bf4ae03bb..c8084997b 100644 --- a/python-test/features/agentProviderUi.feature +++ b/python-test/features/agentProviderUi.feature @@ -5,15 +5,39 @@ Feature: Create agents using orb ui Given the Orb user logs in through the UI And that fleet Management is clickable on ORB Menu And that Agents is clickable on ORB Menu - When a new agent is created through the UI + When a new agent is created through the UI with region:br, demo:true, ns1:true orb tag(s) Then the agents list and the agents view should display agent's status as New within 10 seconds Scenario: Provision agent Given the Orb user logs in through the UI And that fleet Management is clickable on ORB Menu And that Agents is clickable on ORB Menu - When a new agent is created through the UI - And the agent container is started using the command provided by the UI + When a new agent is created through the UI with 2 orb tag(s) + And the agent container is started using the command provided by the UI on port default Then the agents list and the agents view should display agent's status as Online within 10 seconds And the agent status in Orb should be online And the container logs should contain the message "sending capabilities" within 10 seconds + + + Scenario: Run two orb agents on the same port + Given the Orb user logs in through the UI + And that the user is on the orb Agent page + And a new agent is created through the UI with 1 orb tag(s) + And the agent container is started using the command provided by the UI on port default + And that the user is on the orb Agent page + When a new agent is created through the UI with 1 orb tag(s) + And the agent container is started using the command provided by the UI on port default + Then last container created is exited after 2 seconds + And the container logs should contain the message "agent startup error" within 2 seconds + And container on port default is running after 2 seconds + + Scenario: Run two orb agents on the same port + Given the Orb user logs in through the UI + And that the user is on the orb Agent page + And a new agent is created through the UI with 1 orb tag(s) + And the agent container is started using the command provided by the UI on port default + And that the user is on the orb Agent page + When a new agent is created through the UI with 1 orb tag(s) + And the agent container is started using the command provided by the UI on port 10854 + Then last container created is running after 2 seconds + And container on port default is running after 2 seconds \ No newline at end of file diff --git a/python-test/features/agentsProvider.feature b/python-test/features/agentsProvider.feature index dc36d8f39..e7633d98b 100644 --- a/python-test/features/agentsProvider.feature +++ b/python-test/features/agentsProvider.feature @@ -4,8 +4,99 @@ Feature: agent provider Scenario: Provision agent Given the Orb user has a registered account And the Orb user logs in - When a new agent is created - And the agent container is started + When a new agent is created with 1 orb tag(s) + And the agent container is started on port default Then the agent status in Orb should be online And the container logs should contain the message "sending capabilities" within 10 seconds + Scenario: Run two orb agents on the same port + Given the Orb user has a registered account + And the Orb user logs in + And that an agent with 1 orb tag(s) already exists and is online + When a new agent is created with 1 orb tag(s) + And the agent container is started on port default + Then last container created is exited after 2 seconds + And the container logs should contain the message "agent startup error" within 2 seconds + And container on port default is running after 2 seconds + + Scenario: Run two orb agents on different ports + Given the Orb user has a registered account + And the Orb user logs in + And that an agent with 1 orb tag(s) already exists and is online + When a new agent is created with 1 orb tag(s) + And the agent container is started on port 10854 + Then last container created is running after 2 seconds + And container on port default is running after 2 seconds + + + Scenario: Provision agent without tags + Given the Orb user has a registered account + And the Orb user logs in + When a new agent is created with 0 orb tag(s) + And the agent container is started on port default + Then the agent status in Orb should be online + And the container logs should contain the message "sending capabilities" within 10 seconds + + + Scenario: Provision agent with multiple tags + Given the Orb user has a registered account + And the Orb user logs in + When a new agent is created with 5 orb tag(s) + And the agent container is started on port default + Then the agent status in Orb should be online + And the container logs should contain the message "sending capabilities" within 10 seconds + + + Scenario: Edit agent tag + Given the Orb user has a registered account + And the Orb user logs in + And a new agent is created with 5 orb tag(s) + And the agent container is started on port default + When edit the agent tags and use 3 orb tag(s) + Then the container logs should contain the message "sending capabilities" within 10 seconds + And agent must have 3 tags + And the agent status in Orb should be online + + + Scenario: Save agent without tag + Given the Orb user has a registered account + And the Orb user logs in + And a new agent is created with 5 orb tag(s) + And the agent container is started on port default + When edit the agent tags and use 0 orb tag(s) + Then the container logs should contain the message "sending capabilities" within 10 seconds + And agent must have 0 tags + And the agent status in Orb should be online + + + Scenario: Insert tags in agents created without tags + Given the Orb user has a registered account + And the Orb user logs in + And a new agent is created with 0 orb tag(s) + And the agent container is started on port default + When edit the agent tags and use 2 orb tag(s) + Then the container logs should contain the message "sending capabilities" within 10 seconds + And agent must have 2 tags + And the agent status in Orb should be online + + + Scenario: Edit agent name + Given the Orb user has a registered account + And the Orb user logs in + And a new agent is created with 1 orb tag(s) + And the agent container is started on port default + When edit the agent name + Then the container logs should contain the message "sending capabilities" within 10 seconds + And agent must have 1 tags + And the agent status in Orb should be online + + + Scenario: Edit agent name and tags + Given the Orb user has a registered account + And the Orb user logs in + And a new agent is created with 1 orb tag(s) + And the agent container is started on port default + When edit the agent name and edit agent tags using 3 orb tag(s) + Then the container logs should contain the message "sending capabilities" within 10 seconds + And agent must have 3 tags + And the agent status in Orb should be online \ No newline at end of file diff --git a/python-test/features/datasets.feature b/python-test/features/datasets.feature index f06040af9..ac4abf606 100644 --- a/python-test/features/datasets.feature +++ b/python-test/features/datasets.feature @@ -4,7 +4,7 @@ Feature: datasets creation Scenario: Create Dataset Given the Orb user has a registered account And the Orb user logs in - And that an agent already exists and is online + And that an agent with 1 orb tag(s) already exists and is online And referred agent is subscribed to a group And that a sink already exists And that a policy already exists diff --git a/python-test/features/environment.py b/python-test/features/environment.py index 00d5e9690..a5f59bc56 100644 --- a/python-test/features/environment.py +++ b/python-test/features/environment.py @@ -4,6 +4,16 @@ def before_scenario(context, scenario): cleanup_container() + context.execute_steps(''' + Given the Orb user logs in + Then cleanup agents + Then cleanup agent group + Then cleanup sinks + Then cleanup policies + Then cleanup datasets + ''') + context.containers_id = dict() + context.agent_groups = dict() def after_feature(context, feature): @@ -20,7 +30,9 @@ def after_feature(context, feature): def cleanup_container(): docker_client = docker.from_env() - containers = docker_client.containers.list(filters={"name": test_config.LOCAL_AGENT_CONTAINER_NAME}) - if len(containers) == 1: - containers[0].stop() - containers[0].remove() + containers = docker_client.containers.list(all=True) + for container in containers: + test_container = container.name.startswith(test_config.LOCAL_AGENT_CONTAINER_NAME) + if test_container is True: + container.stop() + container.remove() diff --git a/python-test/features/integration.feature b/python-test/features/integration.feature index d41faf3d2..1cf2d22c3 100644 --- a/python-test/features/integration.feature +++ b/python-test/features/integration.feature @@ -4,10 +4,10 @@ Feature: Integration tests Scenario: Apply two policies to an agent Given the Orb user has a registered account And the Orb user logs in - And that an agent already exists and is online + And that an agent with 1 orb tag(s) already exists and is online And referred agent is subscribed to a group And that a sink already exists - When 2 policies are applied to the agent + When 2 policies are applied to the group Then this agent's heartbeat shows that 2 policies are successfully applied And the container logs contain the message "policy applied successfully" referred to each policy within 10 seconds And the container logs that were output after all policies have been applied contain the message "scraped metrics for policy" referred to each applied policy within 180 seconds @@ -15,13 +15,41 @@ Scenario: Apply two policies to an agent And datasets related to all existing policies have validity valid -Scenario: Remove policy from agent +Scenario: apply one policy using multiple datasets to the same group Given the Orb user has a registered account And the Orb user logs in - And that an agent already exists and is online + And that an agent with 2 orb tag(s) already exists and is online And referred agent is subscribed to a group And that a sink already exists - And 2 policies are applied to the agent + When 2 policies are applied to the group by 3 datasets each + Then this agent's heartbeat shows that 2 policies are successfully applied + And 3 datasets are linked with each policy on agent's heartbeat + And the container logs contain the message "policy applied successfully" referred to each policy within 10 seconds + And the container logs that were output after all policies have been applied contain the message "scraped metrics for policy" referred to each applied policy within 180 seconds + And referred sink must have active state on response within 10 seconds + And datasets related to all existing policies have validity valid + + +Scenario: Remove group to which agent is linked + Given the Orb user has a registered account + And the Orb user logs in + And that an agent with 1 orb tag(s) already exists and is online + And referred agent is subscribed to a group + And that a sink already exists + And 1 policies are applied to the group + And this agent's heartbeat shows that 1 policies are successfully applied + When the group to which the agent is linked is removed + Then the container logs should contain the message "completed RPC unsubscription to group" within 10 seconds + And dataset related have validity invalid + + + Scenario: Remove policy from agent + Given the Orb user has a registered account + And the Orb user logs in + And that an agent with 3 orb tag(s) already exists and is online + And referred agent is subscribed to a group + And that a sink already exists + And 2 policies are applied to the group And this agent's heartbeat shows that 2 policies are successfully applied When one of applied policies is removed Then referred policy must not be listed on the orb policies list @@ -36,10 +64,10 @@ Scenario: Remove policy from agent Scenario: Remove dataset from agent with just one dataset linked Given the Orb user has a registered account And the Orb user logs in - And that an agent already exists and is online + And that an agent with 3 orb tag(s) already exists and is online And referred agent is subscribed to a group And that a sink already exists - And 1 policies are applied to the agent + And 1 policies are applied to the group And this agent's heartbeat shows that 1 policies are successfully applied When a dataset linked to this agent is removed Then referred dataset must not be listed on the orb datasets list @@ -52,14 +80,234 @@ Scenario: Remove dataset from agent with just one dataset linked Scenario: Remove dataset from agent with more than one dataset linked Given the Orb user has a registered account And the Orb user logs in - And that an agent already exists and is online + And that an agent with 4 orb tag(s) already exists and is online And referred agent is subscribed to a group And that a sink already exists - And 3 policies are applied to the agent + And 3 policies are applied to the group And this agent's heartbeat shows that 3 policies are successfully applied When a dataset linked to this agent is removed Then referred dataset must not be listed on the orb datasets list And this agent's heartbeat shows that 2 policies are successfully applied And container logs should inform that removed policy was stopped and removed within 10 seconds And the container logs that were output after removing dataset contain the message "scraped metrics for policy" referred to each applied policy within 180 seconds - And the container logs that were output after removing dataset does not contain the message "scraped metrics for policy" referred to deleted policy anymore \ No newline at end of file + And the container logs that were output after removing dataset does not contain the message "scraped metrics for policy" referred to deleted policy anymore + + +Scenario: Provision agent with tags matching an existent group + Given the Orb user has a registered account + And the Orb user logs in + And an Agent Group is created with 2 orb tag(s) + When a new agent is created with tags matching an existing group + And the agent container is started on port default + Then the agent status in Orb should be online + And the container logs should contain the message "completed RPC subscription to group" within 10 seconds + + +Scenario: Provision agent with tag matching existing group linked to a valid dataset + Given the Orb user has a registered account + And the Orb user logs in + And an Agent Group is created with 3 orb tag(s) + And that a sink already exists + And 2 policies are applied to the group + When a new agent is created with tags matching an existing group + And the agent container is started on port default + Then this agent's heartbeat shows that 2 policies are successfully applied + And the container logs contain the message "policy applied successfully" referred to each policy within 10 seconds + And the container logs that were output after all policies have been applied contain the message "scraped metrics for policy" referred to each applied policy within 180 seconds + And referred sink must have active state on response within 10 seconds + And datasets related to all existing policies have validity valid + + +Scenario: Sink with invalid endpoint + Given the Orb user has a registered account + And the Orb user logs in + And that an agent with 1 orb tag(s) already exists and is online + And referred agent is subscribed to a group + And that a sink with invalid endpoint already exists + And that a policy already exists + When a new dataset is created using referred group, sink and policy ID + Then the container logs should contain the message "managing agent policy from core" within 10 seconds + And the container logs should contain the message "policy applied successfully" within 10 seconds + And the container logs should contain the message "scraped metrics for policy" within 180 seconds + And referred sink must have error state on response within 10 seconds + And dataset related have validity valid + + +Scenario: Sink with invalid username + Given the Orb user has a registered account + And the Orb user logs in + And that an agent with 1 orb tag(s) already exists and is online + And referred agent is subscribed to a group + And that a sink with invalid username already exists + And that a policy already exists + When a new dataset is created using referred group, sink and policy ID + Then the container logs should contain the message "managing agent policy from core" within 10 seconds + And the container logs should contain the message "policy applied successfully" within 10 seconds + And the container logs should contain the message "scraped metrics for policy" within 180 seconds + And referred sink must have error state on response within 10 seconds + And dataset related have validity valid + + +Scenario: Sink with invalid password + Given the Orb user has a registered account + And the Orb user logs in + And that an agent with 1 orb tag(s) already exists and is online + And referred agent is subscribed to a group + And that a sink with invalid password already exists + And that a policy already exists + When a new dataset is created using referred group, sink and policy ID + Then the container logs should contain the message "managing agent policy from core" within 10 seconds + And the container logs should contain the message "policy applied successfully" within 10 seconds + And the container logs should contain the message "scraped metrics for policy" within 180 seconds + And referred sink must have error state on response within 10 seconds + And dataset related have validity valid + + +Scenario: Agent subscription to multiple groups created after provisioning agent + Given the Orb user has a registered account + And the Orb user logs in + And a new agent is created with region:br, demo:true, ns1:true orb tag(s) + And the agent container is started on port default + When an Agent Group is created with demo:true, ns1:true orb tag(s) + And an Agent Group is created with region:br orb tag(s) + And an Agent Group is created with demo:true orb tag(s) + Then the container logs contain the message "completed RPC subscription to group" referred to each matching group within 10 seconds + + +Scenario: Agent subscription to multiple groups created before provisioning agent + Given the Orb user has a registered account + And the Orb user logs in + And an Agent Group is created with demo:true, ns1:true orb tag(s) + And an Agent Group is created with region:br orb tag(s) + And an Agent Group is created with demo:true orb tag(s) + When a new agent is created with demo:true, ns1:true orb tag(s) + And the agent container is started on port default + Then the container logs contain the message "completed RPC subscription to group" referred to each matching group within 10 seconds + + +Scenario: Agent subscription to group after editing agent's tags (agent provisioned before editing and group created after) + Given the Orb user has a registered account + And the Orb user logs in + And a new agent is created with demo:true, ns1:true orb tag(s) + And the agent container is started on port default + And an Agent Group is created with demo:true, ns1:true orb tag(s) + When edit the agent tags and use region:br orb tag(s) + And an Agent Group is created with region:br orb tag(s) + Then the container logs contain the message "completed RPC subscription to group" referred to each matching group within 10 seconds + And this agent's heartbeat shows that 1 groups are matching the agent + + +Scenario: Agent subscription to group after editing agent's tags (editing tags after agent provision) + Given the Orb user has a registered account + And the Orb user logs in + And an Agent Group is created with demo:true, ns1:true orb tag(s) + And an Agent Group is created with region:br orb tag(s) + And a new agent is created with demo:true, ns1:true orb tag(s) + And the agent container is started on port default + When edit the agent tags and use region:br orb tag(s) + Then the container logs contain the message "completed RPC subscription to group" referred to each matching group within 10 seconds + And this agent's heartbeat shows that 1 groups are matching the agent + + +Scenario: Agent subscription to group after editing agent's tags (editing tags before agent provision) + Given the Orb user has a registered account + And the Orb user logs in + And a new agent is created with demo:true, ns1:true orb tag(s) + And edit the agent tags and use region:br orb tag(s) + And the agent container is started on port default + When an Agent Group is created with region:br orb tag(s) + Then the container logs contain the message "completed RPC subscription to group" referred to each matching group within 10 seconds + And this agent's heartbeat shows that 1 groups are matching the agent + + +Scenario: Agent subscription to multiple group with policies after editing agent's tags (editing tags after agent provision) + Given the Orb user has a registered account + And the Orb user logs in + And that an agent with test:true orb tag(s) already exists and is online + And referred agent is subscribed to a group + And that a sink already exists + And 2 policies are applied to the group + And this agent's heartbeat shows that 2 policies are successfully applied + And an Agent Group is created with region:br orb tag(s) + And 1 policies are applied to the group + When edit the agent tags and use region:br, test:true orb tag(s) + Then the container logs contain the message "completed RPC subscription to group" referred to each matching group within 10 seconds + And this agent's heartbeat shows that 3 policies are successfully applied + And this agent's heartbeat shows that 2 groups are matching the agent + And the container logs contain the message "policy applied successfully" referred to each policy within 10 seconds + And the container logs that were output after all policies have been applied contain the message "scraped metrics for policy" referred to each applied policy within 180 seconds + + +Scenario: Agent subscription to group with policies after editing agent's tags (editing tags after agent provision) + Given the Orb user has a registered account + And the Orb user logs in + And that an agent with 1 orb tag(s) already exists and is online + And referred agent is subscribed to a group + And that a sink already exists + And 2 policies are applied to the group + And this agent's heartbeat shows that 2 policies are successfully applied + And an Agent Group is created with region:br orb tag(s) + And 1 policies are applied to the group + When edit the agent tags and use region:br orb tag(s) + Then the container logs contain the message "completed RPC subscription to group" referred to each matching group within 10 seconds + And this agent's heartbeat shows that 1 policies are successfully applied + And this agent's heartbeat shows that 1 groups are matching the agent + And the container logs contain the message "policy applied successfully" referred to each policy within 10 seconds + And the container logs that were output after all policies have been applied contain the message "scraped metrics for policy" referred to each applied policy within 180 seconds + + +Scenario: Insert tags in agents created without tags and apply policies to group matching new tags + Given the Orb user has a registered account + And the Orb user logs in + And a new agent is created with 0 orb tag(s) + And the agent container is started on port default + And that a sink already exists + When edit the agent tags and use 2 orb tag(s) + And an Agent Group is created with same tag as the agent + And 1 policies are applied to the group + Then this agent's heartbeat shows that 1 policies are successfully applied + And the container logs contain the message "completed RPC subscription to group" referred to each matching group within 10 seconds + And this agent's heartbeat shows that 1 groups are matching the agent + And the container logs contain the message "policy applied successfully" referred to each policy within 10 seconds + And the container logs that were output after all policies have been applied contain the message "scraped metrics for policy" referred to each applied policy within 180 seconds + + +Scenario: Edit agent name and apply policies to then + Given the Orb user has a registered account + And the Orb user logs in + And that an agent with 5 orb tag(s) already exists and is online + And an Agent Group is created with same tag as the agent + And 1 agent must be matching on response field matching_agents + And that a sink already exists + And 1 policies are applied to the group + When edit the agent name and edit agent tags using 3 orb tag(s) + Then this agent's heartbeat shows that 1 policies are successfully applied + And the container logs contain the message "policy applied successfully" referred to each policy within 10 seconds + + +Scenario: Editing tags of an Agent Group with policies (unsubscription) + Given the Orb user has a registered account + And the Orb user logs in + And that an agent with region:br orb tag(s) already exists and is online + And an Agent Group is created with same tag as the agent and without description + And that a sink already exists + And 2 policies are applied to the group + When the name, tags, description of Agent Group is edited using: name=new_name/ tags=another:tag, ns1:true/ description=None + Then 0 agent must be matching on response field matching_agents + And the container logs should contain the message "completed RPC unsubscription to group" within 10 seconds + And the agent status in Orb should be online + + +Scenario: Editing tags of an Agent Group with policies (subscription) + Given the Orb user has a registered account + And the Orb user logs in + And that an agent with region:br, another:tag orb tag(s) already exists and is online + And an Agent Group is created with ns1:true orb tag(s) and without description + And that a sink already exists + And 2 policies are applied to the group + When the name, tags, description of Agent Group is edited using: name=new_name/ tags=region:br/ description=None + Then 1 agent must be matching on response field matching_agents + And the container logs should contain the message "completed RPC subscription to group" within 10 seconds + And the agent status in Orb should be online + And this agent's heartbeat shows that 1 groups are matching the agent + And this agent's heartbeat shows that 2 policies are successfully applied diff --git a/python-test/features/login.feature b/python-test/features/login.feature index 453336651..e7cff47f0 100644 --- a/python-test/features/login.feature +++ b/python-test/features/login.feature @@ -1,18 +1,16 @@ @login Feature: login tests - Scenario Outline: Request registration of a registered email using password + Scenario Outline: Request registration of a registered email using password Given there is a registered account When request referred account registration using registered email, password, user name and company name Examples: | password_status | username | company | | registered | Tester | NS1 | - | registered | Tester | NS1 | | registered | None | None | | registered | Tester | None | | registered | None | NS1 | | unregistered | Tester | NS1 | - | unregistered | Tester | NS1 | | unregistered | None | None | | unregistered | Tester | None | | unregistered | None | NS1 | @@ -27,3 +25,32 @@ | incorrect | correct | | correct | incorrect | Then user should not be able to authenticate + + Scenario Outline: Check if email is a required field + When user request account registration email, password, user name and company name + Examples: + | email | password | username | company | + | without | with | with | with | + | without | with | with | without | + | without | with | without | without | + | without | with | without | with | + | without | without | without | with | + | without | without | without | without | + | without | without | with | with | + | without | without | with | without | + Then user should not be able to authenticate + + + Scenario Outline: Check if password is a required field + When user request account registration email, password, user name and company name + Examples: + | password | email | username | company | + | without | with | with | with | + | without | with | with | without | + | without | with | without | without | + | without | with | without | with | + | without | without | without | with | + | without | without | without | without | + | without | without | with | with | + | without | without | with | without | + Then user should not be able to authenticate diff --git a/python-test/features/policies.feature b/python-test/features/policies.feature index 237a97886..11dc97e7f 100644 --- a/python-test/features/policies.feature +++ b/python-test/features/policies.feature @@ -4,6 +4,6 @@ Feature: policy creation Scenario: Create a policy Given the Orb user has a registered account And the Orb user logs in - And that an agent already exists and is online + And that an agent with 1 orb tag(s) already exists and is online When a new policy is created Then referred policy must be listed on the orb policies list \ No newline at end of file diff --git a/python-test/features/steps/control_agents_ui.py b/python-test/features/steps/control_agents_ui.py index d64a01adc..38e935ec9 100644 --- a/python-test/features/steps/control_agents_ui.py +++ b/python-test/features/steps/control_agents_ui.py @@ -1,13 +1,14 @@ -from behave import given, when, then +from behave import given, then, step from ui_utils import input_text_by_xpath -from control_plane_agents import agent_name_prefix, tag_key_prefix, tag_value_prefix +from control_plane_agents import agent_name_prefix from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By -from utils import random_string +from utils import random_string, create_tags_set from test_config import TestConfig import time from hamcrest import * +from page_objects import * configs = TestConfig.configs() base_orb_url = configs.get('base_orb_url') @@ -15,60 +16,68 @@ @given("that fleet Management is clickable on ORB Menu") def expand_fleet_management(context): - context.driver.find_elements_by_xpath("//a[contains(@title, 'Fleet Management')]")[0].click() + context.driver.find_elements_by_xpath(LeftMenu.fleet_management())[0].click() @given('that Agents is clickable on ORB Menu') def agent_page(context): WebDriverWait(context.driver, 3).until( - EC.element_to_be_clickable((By.XPATH, "//a[contains(@title, 'Agents')]"))) - context.driver.find_element_by_xpath("//a[contains(@title, 'Agents')]").click() + EC.element_to_be_clickable((By.XPATH, LeftMenu.agents()))) + context.driver.find_element_by_xpath(LeftMenu.agents()).click() WebDriverWait(context.driver, 5).until(EC.url_to_be(f"{base_orb_url}/pages/fleet/agents"), message="Orb agents " "page not " "available") -@when("a new agent is created through the UI") -def create_agent_through_the_agents_page(context): +@step("that the user is on the orb Agent page") +def orb_page(context): + expand_fleet_management(context) + agent_page(context) + current_url = context.driver.current_url + assert_that(current_url, equal_to(f"{base_orb_url}/pages/fleet/agents"), + "user not enabled to access orb login page") + + +@step("a new agent is created through the UI with {orb_tags} orb tag(s)") +def create_agent_through_the_agents_page(context, orb_tags): + context.orb_tags = create_tags_set(orb_tags) WebDriverWait(context.driver, 3).until( - EC.element_to_be_clickable((By.XPATH, "//button[contains(text(), 'New Agent')]"))).click() + EC.element_to_be_clickable((By.XPATH, AgentsPage.new_agent_button()))).click() WebDriverWait(context.driver, 5).until(EC.url_to_be(f"{base_orb_url}/pages/fleet/agents/add"), message="Orb add" "agents " "page not " "available") context.agent_name = agent_name_prefix + random_string(10) - context.agent_tag_key = tag_key_prefix + random_string(4) - context.agent_tag_value = tag_value_prefix + random_string(4) - input_text_by_xpath("//input[contains(@data-orb-qa-id, 'input#name')]", context.agent_name, context) + input_text_by_xpath(AgentsPage.agent_name(), context.agent_name, context) WebDriverWait(context.driver, 3).until( - EC.element_to_be_clickable((By.XPATH, "//button[contains(text(), 'Next')]"))).click() - input_text_by_xpath("//input[contains(@data-orb-qa-id, 'input#orb_tag_key')]", context.agent_tag_key, context) - input_text_by_xpath("//input[contains(@data-orb-qa-id, 'input#orb_tag_value')]", context.agent_tag_value, context) - WebDriverWait(context.driver, 3).until( - EC.element_to_be_clickable((By.XPATH, "//button[contains(@data-orb-qa-id, 'button#addTag')]"))).click() + EC.element_to_be_clickable((By.XPATH, UtilButton.next_button()))).click() + for tag_key, tag_value in context.orb_tags.items(): + input_text_by_xpath(AgentsPage.agent_tag_key(), tag_key, context) + input_text_by_xpath(AgentsPage.agent_tag_value(), tag_value, context) + WebDriverWait(context.driver, 3).until( + EC.element_to_be_clickable((By.XPATH, AgentsPage.agent_add_tag_button()))).click() WebDriverWait(context.driver, 3).until( - EC.element_to_be_clickable((By.XPATH, "//button[contains(text(), 'Next')]"))).click() + EC.element_to_be_clickable((By.XPATH, UtilButton.next_button()))).click() WebDriverWait(context.driver, 3).until( - EC.element_to_be_clickable((By.XPATH, "//button[contains(text(), 'Save')]"))).click() + EC.element_to_be_clickable((By.XPATH, UtilButton.save_button()))).click() WebDriverWait(context.driver, 3).until( EC.text_to_be_present_in_element((By.CSS_SELECTOR, "span.title"), 'Agent successfully created')) - agent_key_xpath = "//label[contains(text(), 'Agent Key')]/following::pre[1]" context.agent_key = \ - WebDriverWait(context.driver, 3).until(EC.presence_of_all_elements_located((By.XPATH, agent_key_xpath)))[0].text - agent_provisioning_command_xpath = "//label[contains(text(), 'Provisioning Command')]/following::pre[1]" + WebDriverWait(context.driver, 3).until(EC.presence_of_all_elements_located((By.XPATH, + AgentsPage.agent_key())))[0].text agent_provisioning_command = \ WebDriverWait(context.driver, 3).until( - EC.presence_of_all_elements_located((By.XPATH, agent_provisioning_command_xpath)))[0].text + EC.presence_of_all_elements_located((By.XPATH, AgentsPage.agent_provisioning_command())))[0].text context.agent_provisioning_command = agent_provisioning_command.replace("\n\n", " ") WebDriverWait(context.driver, 3).until( - EC.presence_of_all_elements_located((By.XPATH, "//span[contains(@class, 'nb-close')]")))[0].click() + EC.presence_of_all_elements_located((By.XPATH, UtilButton.close_button())))[0].click() WebDriverWait(context.driver, 3).until( EC.presence_of_all_elements_located((By.XPATH, f"//div[contains(@class, 'agent-name') and contains(text()," - f"{context.agent_name})]")))[0].click() + f"'{context.agent_name}')]")))[0].click() context.agent = dict() context.agent['id'] = WebDriverWait(context.driver, 3).until( - EC.presence_of_all_elements_located((By.XPATH, "//label[contains(text(), 'Agent ID')]/following::p")))[0].text + EC.presence_of_all_elements_located((By.XPATH, AgentsPage.agent_view_id())))[0].text @then("the agents list and the agents view should display agent's status as {status} within {time_to_wait} seconds") @@ -82,7 +91,7 @@ def check_status_on_orb_ui(context, status, time_to_wait): while time_waiting < timeout: list_of_datatable_body_cell = WebDriverWait(context.driver, 3).until( - EC.presence_of_all_elements_located([By.XPATH, f"//div[contains(text(), {context.agent_name})]/ancestor" + EC.presence_of_all_elements_located([By.XPATH, f"//div[contains(text(), '{context.agent_name}')]/ancestor" f"::datatable-body-row/descendant::i[contains(@class, " f"'fa fa-circle')]/ancestor::div[contains(@class, " f"'ng-star-inserted')]"])) @@ -96,8 +105,8 @@ def check_status_on_orb_ui(context, status, time_to_wait): assert_that(list_of_datatable_body_cell[1].text, equal_to(status)) WebDriverWait(context.driver, 3).until( EC.presence_of_all_elements_located((By.XPATH, f"//div[contains(@class, 'agent-name') and contains(text()," - f"{context.agent_name})]")))[0].click() + f"'{context.agent_name}')]")))[0].click() agent_view_status = WebDriverWait(context.driver, 3).until( EC.presence_of_all_elements_located( - (By.XPATH, "//label[contains(text(), 'Health Status')]/following::p")))[0].text + (By.XPATH, AgentsPage.agent_status())))[0].text assert_that(agent_view_status, equal_to(status), f"Agent {context.agent['id']} status failed") diff --git a/python-test/features/steps/control_plane_agent_groups.py b/python-test/features/steps/control_plane_agent_groups.py index 9a0a4df33..0e264c6bd 100644 --- a/python-test/features/steps/control_plane_agent_groups.py +++ b/python-test/features/steps/control_plane_agent_groups.py @@ -1,10 +1,12 @@ from test_config import TestConfig -from local_agent import run_local_agent_container +from local_agent import get_orb_agent_logs from users import get_auth_token -from utils import random_string, filter_list_by_parameter_start_with -from behave import given, when, then +from utils import random_string, filter_list_by_parameter_start_with, generate_random_string_with_predefined_prefix, \ + create_tags_set, check_logs_contain_message_and_name +from behave import given, then, step from hamcrest import * import requests +import time configs = TestConfig.configs() agent_group_name_prefix = 'test_group_name_' @@ -12,19 +14,119 @@ base_orb_url = configs.get('base_orb_url') -@when("an Agent Group is created with same tag as the agent") -def creat_agent_group(context): +@step("an Agent Group is created with same tag as the agent") +def create_agent_group_matching_agent(context, **kwargs): agent_group_name = agent_group_name_prefix + random_string() - context.agent_group_data = create_agent_group(context.token, agent_group_name, agent_group_description, - context.agent_tag_key, context.agent_tag_value) + if "group_description" in kwargs.keys(): + group_description = kwargs["group_description"] + else: + group_description = agent_group_description + tags = context.agent["orb_tags"] + context.agent_group_data = create_agent_group(context.token, agent_group_name, group_description, + tags) + group_id = context.agent_group_data['id'] + context.agent_groups[group_id] = agent_group_name -@then("one agent must be matching on response field matching_agents") -def matching_agent(context): +@step("an Agent Group is created with {orb_tags} orb tag(s)") +def create_new_agent_group(context, orb_tags, **kwargs): + agent_group_name = generate_random_string_with_predefined_prefix(agent_group_name_prefix) + if "group_description" in kwargs.keys(): + group_description = kwargs["group_description"] + else: + group_description = agent_group_description + context.orb_tags = create_tags_set(orb_tags) + if len(context.orb_tags) == 0: + context.agent_group_data = create_agent_group(context.token, agent_group_name, group_description, + context.orb_tags, 400) + else: + context.agent_group_data = create_agent_group(context.token, agent_group_name, group_description, + context.orb_tags) + group_id = context.agent_group_data['id'] + context.agent_groups[group_id] = agent_group_name + + +@step("an Agent Group is created with {orb_tags} orb tag(s) and {description} description") +def create_new_agent_group_with_defined_description(context, orb_tags, description): + if description == "without": + create_new_agent_group(context, orb_tags, group_description=None) + else: + description = description.replace('"', '') + description = description.replace(' as', '') + create_new_agent_group(context, orb_tags, group_description=description) + + +@step("an Agent Group is created with same tag as the agent and {description} description") +def create_agent_group_with_defined_description_and_matching_agent(context, description): + if description == "without": + create_agent_group_matching_agent(context, group_description=None) + else: + description = description.replace('"', '') + description = description.replace(' as', '') + create_agent_group_matching_agent(context, group_description=description) + + +@step("the {edited_parameters} of Agent Group is edited using: {parameters_values}") +def edit_multiple_groups_parameters(context, edited_parameters, parameters_values): + edited_parameters = edited_parameters.split(", ") + for param in edited_parameters: + assert_that(param, any_of(equal_to('name'), equal_to('description'), equal_to('tags')), + 'Unexpected parameter to edit') + parameters_values = parameters_values.split("/ ") + + group_editing = get_agent_group(context.token, context.agent_group_data["id"]) + group_data = {"name": group_editing["name"], "tags": group_editing["tags"]} + if "description" in group_editing.keys(): + group_data["description"] = group_editing["description"] + else: + group_data["description"] = None + + editing_param_dict = dict() + for param in parameters_values: + param_split = param.split("=") + if param_split[1].lower() == "none": + param_split[1] = None + editing_param_dict[param_split[0]] = param_split[1] + + assert_that(set(editing_param_dict.keys()), equal_to(set(edited_parameters)), + "All parameter must have referenced value") + + if "tags" in editing_param_dict.keys() and editing_param_dict["tags"] is not None: + editing_param_dict["tags"] = create_tags_set(editing_param_dict["tags"]) + if "name" in editing_param_dict.keys() and editing_param_dict["name"] is not None: + editing_param_dict["name"] = agent_group_name_prefix + editing_param_dict["name"] + + for parameter, value in editing_param_dict.items(): + group_data[parameter] = value + + context.editing_response = edit_agent_group(context.token, context.agent_group_data["id"], group_data["name"], + group_data["description"], group_data["tags"]) + + +@then("agent group editing must fail") +def fail_group_editing(context): + assert_that(list(context.editing_response.keys())[0], equal_to("error")) + + +@step("Agent Group creation response must be an error with message '{message}'") +def error_response_message(context, message): + response = list(context.agent_group_data.items())[0] + response_key, response_value = response[0], response[1] + assert_that(response_key, equal_to('error'), + 'Response of invalid agent group creation must be an error') + assert_that(response_value, equal_to(message), "Unexpected message for error") + + +@step("{amount_agent_matching} agent must be matching on response field matching_agents") +def matching_agent(context, amount_agent_matching): + context.agent_group_data = get_agent_group(context.token, context.agent_group_data["id"]) matching_total_agents = context.agent_group_data['matching_agents']['total'] - matching_online_agents = context.agent_group_data['matching_agents']['online'] - assert_that(matching_total_agents, equal_to(1)) - assert_that(matching_online_agents, equal_to(1)) + assert_that(matching_total_agents, equal_to(int(amount_agent_matching))) + + +@step("the group to which the agent is linked is removed") +def remove_group(context): + delete_agent_group(context.token, context.agent_group_data['id']) @then('cleanup agent group') @@ -43,36 +145,72 @@ def clean_agent_groups(context): @given("referred agent is subscribed to a group") def subscribe_agent_to_a_group(context): agent = context.agent - agent_group_name = agent_group_name_prefix + random_string(4) - agent_tag_key = list(agent['orb_tags'].keys())[0] - agent_tag_value = agent['orb_tags'][agent_tag_key] - context.agent_group_data = create_agent_group(context.token, agent_group_name, agent_group_description, - agent_tag_key, agent_tag_value) - matching_agent(context) + agent_group_name = generate_random_string_with_predefined_prefix(agent_group_name_prefix) + agent_tags = agent['orb_tags'] + context.agent_group_data = create_agent_group(context.token, agent_group_name, agent_group_description, agent_tags) + group_id = context.agent_group_data['id'] + context.agent_groups[group_id] = agent_group_name + +@step('the container logs contain the message "{text_to_match}" referred to each matching group within' + '{time_to_wait} seconds') +def check_logs_for_group(context, text_to_match, time_to_wait): + groups_matching = list() + context.groups_matching_id = list() + for group in context.agent_groups.keys(): + group_data = get_agent_group(context.token, group) + group_tags = dict(group_data["tags"]) + agent_tags = context.agent["orb_tags"] + if all(item in agent_tags.items() for item in group_tags.items()) is True: + groups_matching.append(context.agent_groups[group]) + context.groups_matching_id.append(group) + text_found, groups_to_which_subscribed = check_subscription(time_to_wait, groups_matching, + text_to_match, context.container_id) + assert_that(text_found, is_(True), f"Message {text_to_match} was not found in the agent logs for group(s)" + f"{set(groups_matching).difference(groups_to_which_subscribed)}!") -def create_agent_group(token, name, description, tag_key, tag_value): + +def create_agent_group(token, name, description, tags, expected_status_code=201): """ Creates an agent group in Orb control plane :param (str) token: used for API authentication :param (str) name: of the agent to be created :param (str) description: description of group - :param (str) tag_key: the key of the tag to be added to this agent - :param (str) tag_value: the value of the tag to be added to this agent + :param (dict) tags: dict with all pairs key:value that will be used as tags :returns: (dict) a dictionary containing the created agent group data + :param (int) expected_status_code: expected request's status code. Default:201 (happy path). """ - json_request = {"name": name, "description": description, "tags": {tag_key: tag_value}} + json_request = {"name": name, "description": description, "tags": tags} headers_request = {'Content-type': 'application/json', 'Accept': '*/*', 'Authorization': token} response = requests.post(base_orb_url + '/api/v1/agent_groups', json=json_request, headers=headers_request) - assert_that(response.status_code, equal_to(201), - 'Request to create agent failed with status=' + str(response.status_code)) + assert_that(response.status_code, equal_to(expected_status_code), + 'Request to create agent group failed with status=' + str(response.status_code)) return response.json() +def get_agent_group(token, agent_group_id): + """ + Gets an agent group from Orb control plane + + :param (str) token: used for API authentication + :param (str) agent_group_id: that identifies the agent group to be fetched + :returns: (dict) the fetched agent group + """ + + get_groups_response = requests.get(base_orb_url + '/api/v1/agent_groups/' + agent_group_id, + headers={'Authorization': token}) + + assert_that(get_groups_response.status_code, equal_to(200), + 'Request to get agent group id=' + agent_group_id + ' failed with status=' + str( + get_groups_response.status_code)) + + return get_groups_response.json() + + def list_agent_groups(token, limit=100): """ Lists up to 100 agent groups from Orb control plane that belong to this user @@ -117,3 +255,58 @@ def delete_agent_group(token, agent_group_id): assert_that(response.status_code, equal_to(204), 'Request to delete agent group id=' + agent_group_id + ' failed with status=' + str(response.status_code)) + + +def check_subscription(time_to_wait, agent_groups_names, expected_message, container_id): + """ + + :param (int) time_to_wait: timout (seconds) + :param (list) agent_groups_names: groups to which the agent must be subscribed + :param (str) expected_message: message that we expect to find in the logs + :param (str) container_id: agent container id + :return: (bool) True if agent is subscribed to all matching groups, (list) names of the groups to which agent is subscribed + """ + groups_to_which_subscribed = set() + time_waiting = 0 + sleep_time = 0.5 + timeout = int(time_to_wait) + while time_waiting < timeout: + for name in agent_groups_names: + logs = get_orb_agent_logs(container_id) + text_found, log_line = check_logs_contain_message_and_name(logs, expected_message, name, "group_name") + if text_found is True: + groups_to_which_subscribed.add(log_line["group_name"]) + if set(groups_to_which_subscribed) == set(agent_groups_names): + return True, groups_to_which_subscribed + time.sleep(sleep_time) + time_waiting += sleep_time + return False, groups_to_which_subscribed + + +def edit_agent_group(token, agent_group_id, name, description, tags, expected_status_code=200): + """ + + :param (str) token: used for API authentication + :param (str) agent_group_id: that identifies the agent group to be edited + :param (str) name: agent group's name + :param (str) description: agent group's description + :param (str) tags: orb tags that will be used to connect agents to groups + :param (int) expected_status_code: expected request's status code. Default:200. + :returns: (dict) the edited agent group + """ + + json_request = {"name": name, "description": description, "tags": tags, + "validate_only": False} + json_request = {parameter: value for parameter, value in json_request.items() if value} + + headers_request = {'Content-type': 'application/json', 'Accept': '*/*', 'Authorization': token} + + group_edited_response = requests.put(base_orb_url + '/api/v1/agent_groups/' + agent_group_id, json=json_request, + headers=headers_request) + + if name is None or tags is None: + expected_status_code = 400 + assert_that(group_edited_response.status_code, equal_to(expected_status_code), + 'Request to edit agent group failed with status=' + str(group_edited_response.status_code)) + + return group_edited_response.json() diff --git a/python-test/features/steps/control_plane_agents.py b/python-test/features/steps/control_plane_agents.py index f5f6537a4..0ed4b4cb2 100644 --- a/python-test/features/steps/control_plane_agents.py +++ b/python-test/features/steps/control_plane_agents.py @@ -1,5 +1,5 @@ from test_config import TestConfig -from utils import random_string, filter_list_by_parameter_start_with +from utils import random_string, filter_list_by_parameter_start_with, generate_random_string_with_predefined_prefix, create_tags_set from local_agent import run_local_agent_container from behave import given, when, then, step from hamcrest import * @@ -8,33 +8,37 @@ configs = TestConfig.configs() agent_name_prefix = "test_agent_name_" -tag_key_prefix = "test_tag_key_" -tag_value_prefix = "test_tag_value_" base_orb_url = configs.get('base_orb_url') -@given("that an agent already exists and is {status}") -def check_if_agents_exist(context, status): - context.agent_name, context.agent_tag_key, context.agent_tag_value = generate_agent_name_and_tag(agent_name_prefix, - tag_key_prefix, - tag_value_prefix) - agent = create_agent(context.token, context.agent_name, context.agent_tag_key, context.agent_tag_value) - context.agent = agent +@given("that an agent with {orb_tags} orb tag(s) already exists and is {status}") +def check_if_agents_exist(context, orb_tags, status): + context.agent_name = generate_random_string_with_predefined_prefix(agent_name_prefix) + context.orb_tags = create_tags_set(orb_tags) + context.agent = create_agent(context.token, context.agent_name, context.orb_tags) + context.agent_key = context.agent["key"] token = context.token - run_local_agent_container(context) + run_local_agent_container(context, "default") agent_id = context.agent['id'] existing_agents = get_agent(token, agent_id) assert_that(len(existing_agents), greater_than(0), "Agent not created") expect_container_status(token, agent_id, status) -@when('a new agent is created') -def agent_is_created(context): - context.agent_name, context.agent_tag_key, context.agent_tag_value = generate_agent_name_and_tag(agent_name_prefix, - tag_key_prefix, - tag_value_prefix) - agent = create_agent(context.token, context.agent_name, context.agent_tag_key, context.agent_tag_value) +@step('a new agent is created with {orb_tags} orb tag(s)') +def agent_is_created(context, orb_tags): + context.agent_name = generate_random_string_with_predefined_prefix(agent_name_prefix) + context.orb_tags = create_tags_set(orb_tags) + context.agent = create_agent(context.token, context.agent_name, context.orb_tags) + context.agent_key = context.agent["key"] + + +@when('a new agent is created with tags matching an existing group') +def agent_is_created_matching_group(context): + context.agent_name = agent_name_prefix + random_string(10) + agent = create_agent(context.token, context.agent_name, context.orb_tags) context.agent = agent + context.agent_key = context.agent["key"] @then('the agent status in Orb should be {status}') @@ -57,23 +61,89 @@ def clean_agents(context): delete_agents(token, agents_filtered_list) +@step("{amount_of_datasets} datasets are linked with each policy on agent's heartbeat") +def multiple_dataset_for_policy(context, amount_of_datasets): + agent = get_agent(context.token, context.agent['id']) + for policy_id in context.list_agent_policies_id: + assert_that(len(agent['last_hb_data']['policy_state'][policy_id]['datasets']), equal_to(int(amount_of_datasets)), + f"Amount of datasets linked with policy {policy_id} failed") + + @step("this agent's heartbeat shows that {amount_of_policies} policies are successfully applied") def list_policies_applied_to_an_agent(context, amount_of_policies): time_waiting = 0 sleep_time = 0.5 - timeout = 180 - + timeout = 30 + context.list_agent_policies_id = list() while time_waiting < timeout: agent = get_agent(context.token, context.agent['id']) - context.list_agent_policies_id = list(agent['last_hb_data']['policy_state'].keys()) - if len(context.list_agent_policies_id) == int(amount_of_policies): - break + if 'policy_state' in agent['last_hb_data'].keys(): + context.list_agent_policies_id = list(agent['last_hb_data']['policy_state'].keys()) + if len(context.list_agent_policies_id) == int(amount_of_policies): + break time.sleep(sleep_time) time_waiting += sleep_time assert_that(len(context.list_agent_policies_id), equal_to(int(amount_of_policies)), f"Amount of policies applied to this agent failed with {context.list_agent_policies_id} policies") - assert_that(sorted(context.list_agent_policies_id), equal_to(sorted(context.policies_created.keys()))) + assert_that(sorted(context.list_agent_policies_id), equal_to(sorted(context.policies_created.keys())), + "Policies linked with the agent is not the same as the created by test process") + for policy_id in context.list_agent_policies_id: + assert_that(agent['last_hb_data']['policy_state'][policy_id]["state"], equal_to('running'), + f"policy {policy_id} is not running") + + +@step("this agent's heartbeat shows that {amount_of_groups} groups are matching the agent") +def list_groups_matching_an_agent(context, amount_of_groups): + time_waiting = 0 + sleep_time = 0.5 + timeout = 30 + context.list_groups_id = list() + while time_waiting < timeout: + agent = get_agent(context.token, context.agent['id']) + if 'group_state' in agent['last_hb_data'].keys(): + context.list_groups_id = list(agent['last_hb_data']['group_state'].keys()) + if sorted(context.list_groups_id) == sorted(context.groups_matching_id): + break + time.sleep(sleep_time) + time_waiting += sleep_time + + assert_that(len(context.list_groups_id), equal_to(int(amount_of_groups)), + f"Amount of groups matching the agent failed with {context.list_groups_id} groups") + assert_that(sorted(context.list_groups_id), equal_to(sorted(context.groups_matching_id)), + "Groups matching the agent is not the same as the created by test process") + + +@step("edit the agent tags and use {orb_tags} orb tag(s)") +def editing_agent_tags(context, orb_tags): + agent = get_agent(context.token, context.agent["id"]) + context.orb_tags = create_tags_set(orb_tags) + edit_agent(context.token, context.agent["id"], agent["name"], context.orb_tags, expected_status_code=200) + context.agent = get_agent(context.token, context.agent["id"]) + + +@step("edit the agent name") +def editing_agent_name(context): + agent = get_agent(context.token, context.agent["id"]) + agent_new_name = generate_random_string_with_predefined_prefix(agent_name_prefix, 5) + edit_agent(context.token, context.agent["id"], agent_new_name, agent['orb_tags'], expected_status_code=200) + context.agent = get_agent(context.token, context.agent["id"]) + assert_that(context.agent["name"], equal_to(agent_new_name), "Agent name editing failed") + + +@step("edit the agent name and edit agent tags using {orb_tags} orb tag(s)") +def editing_agent_name_and_tags(context, orb_tags): + agent_new_name = generate_random_string_with_predefined_prefix(agent_name_prefix, 5) + context.orb_tags = create_tags_set(orb_tags) + edit_agent(context.token, context.agent["id"], agent_new_name, context.orb_tags, expected_status_code=200) + context.agent = get_agent(context.token, context.agent["id"]) + assert_that(context.agent["name"], equal_to(agent_new_name), "Agent name editing failed") + + +@step("agent must have {amount_of_tags} tags") +def check_agent_tags(context, amount_of_tags): + agent = get_agent(context.token, context.agent["id"]) + assert_that(len(dict(agent["orb_tags"])), equal_to(int(amount_of_tags)), "Amount of orb tags failed") def expect_container_status(token, agent_id, status): @@ -164,18 +234,17 @@ def delete_agent(token, agent_id): + agent_id + ' failed with status=' + str(response.status_code)) -def create_agent(token, name, tag_key, tag_value): +def create_agent(token, name, tags): """ Creates an agent in Orb control plane :param (str) token: used for API authentication :param (str) name: of the agent to be created - :param (str) tag_key: the key of the tag to be added to this agent - :param (str) tag_value: the value of the tag to be added to this agent + :param (dict) tags: orb agent tags :returns: (dict) a dictionary containing the created agent data """ - json_request = {"name": name, "orb_tags": {tag_key: tag_value}, "validate_only": False} + json_request = {"name": name, "orb_tags": tags, "validate_only": False} headers_request = {'Content-type': 'application/json', 'Accept': '*/*', 'Authorization': token} @@ -186,14 +255,21 @@ def create_agent(token, name, tag_key, tag_value): return response.json() -def generate_agent_name_and_tag(name_agent_prefix, agent_tag_key_prefix, agent_tag_value_prefix): +def edit_agent(token, agent_id, name, tags, expected_status_code=200): """ - :param (str) name_agent_prefix: prefix to identify agents created by tests - :param (str) agent_tag_key_prefix: prefix to identify tag_key created by tests - :param (str) agent_tag_value_prefix: prefix to identify tag_value created by tests - :return: random name, tag_key and tag_value for agent + :param (str) token: used for API authentication + :param (str) agent_id: that identifies the agent to be deleted + :param (str) name: of the agent to be created + :param (dict) tags: orb agent tags + :param (int) expected_status_code: expected request's status code. Default:200 (happy path). + :return: (dict) a dictionary containing the edited agent data """ - agent_name = name_agent_prefix + random_string(10) - agent_tag_key = agent_tag_key_prefix + random_string(4) - agent_tag_value = agent_tag_value_prefix + random_string(4) - return agent_name, agent_tag_key, agent_tag_value + + json_request = {"name": name, "orb_tags": tags, "validate_only": False} + headers_request = {'Content-type': 'application/json', 'Accept': '*/*', + 'Authorization': token} + response = requests.put(base_orb_url + '/api/v1/agents/' + agent_id, json=json_request, headers=headers_request) + assert_that(response.status_code, equal_to(expected_status_code), + 'Request to edit agent failed with status=' + str(response.status_code)) + + return response.json() diff --git a/python-test/features/steps/control_plane_datasets.py b/python-test/features/steps/control_plane_datasets.py index 545812fd3..d6d3006f8 100644 --- a/python-test/features/steps/control_plane_datasets.py +++ b/python-test/features/steps/control_plane_datasets.py @@ -72,6 +72,15 @@ def check_dataset_status_valid(context): f"equals {dataset['valid']}") +@step('dataset related have validity {validity}') +def check_dataset_status_valid(context, validity): + assert_that(validity, any_of(equal_to('invalid'), equal_to('valid'))) + validity_bool = {"invalid": False, "valid": True} + dataset = get_dataset(context.token, context.dataset['id']) + assert_that(dataset['valid'], equal_to(validity_bool[validity]), f"dataset {dataset['id']} status failed with " + f"valid equals {dataset['valid']}") + + def create_dataset(token, name_label, policy_id, agent_group_id, sink_id): """ diff --git a/python-test/features/steps/control_plane_policies.py b/python-test/features/steps/control_plane_policies.py index bee9f0d7e..bc2e488fb 100644 --- a/python-test/features/steps/control_plane_policies.py +++ b/python-test/features/steps/control_plane_policies.py @@ -138,7 +138,7 @@ def check_agent_logs_for_policies(context, text_to_match, time_to_wait): f" was not found in the agent logs!") -@step('{amount_of_policies} policies are applied to the agent') +@step('{amount_of_policies} policies are applied to the group') def apply_n_policies(context, amount_of_policies): for i in range(int(amount_of_policies)): create_new_policy(context) @@ -146,6 +146,15 @@ def apply_n_policies(context, amount_of_policies): create_new_dataset(context) +@step('{amount_of_policies} policies are applied to the group by {amount_of_datasets} datasets each') +def apply_n_policies_x_times(context, amount_of_policies, amount_of_datasets): + for n in range(int(amount_of_policies)): + create_new_policy(context) + check_policies(context) + for x in range(int(amount_of_datasets)): + create_new_dataset(context) + + def create_policy(token, policy_name, handler_label, handler, description=None, tap="default_pcap", input_type="pcap", host_specification=None, filter_expression=None, backend_type="pktvisor"): """ diff --git a/python-test/features/steps/control_plane_sink.py b/python-test/features/steps/control_plane_sink.py index fefa084a8..fa54ee1ee 100644 --- a/python-test/features/steps/control_plane_sink.py +++ b/python-test/features/steps/control_plane_sink.py @@ -37,6 +37,20 @@ def create_sink(context): context.sink = create_new_sink(token, sink_label_name, endpoint, username, password) +@step("that a sink with invalid {credential} already exists") +def create_invalid_sink(context, credential): + assert_that(credential, any_of(equal_to('endpoint'), equal_to('username'), equal_to('password')), + "Invalid prometheus field") + check_prometheus_grafana_credentials(context) + sink_label_name = sink_label_name_prefix + random_string(10) + token = context.token + prometheus_credentials = {'endpoint': context.remote_prometheus_endpoint, 'username': context.prometheus_username, + 'password': context.prometheus_key} + prometheus_credentials[credential] = prometheus_credentials[credential][:-1] + context.sink = create_new_sink(token, sink_label_name, prometheus_credentials['endpoint'], + prometheus_credentials['username'], prometheus_credentials['password']) + + @step("referred sink must have {status} state on response within {time_to_wait} seconds") def check_sink_status(context, status, time_to_wait): time_waiting = 0 diff --git a/python-test/features/steps/local_agent.py b/python-test/features/steps/local_agent.py index ae741fca0..4684ea725 100644 --- a/python-test/features/steps/local_agent.py +++ b/python-test/features/steps/local_agent.py @@ -1,5 +1,5 @@ -from utils import safe_load_json -from behave import when, then +from utils import safe_load_json, random_string +from behave import when, then, step from hamcrest import * from test_config import TestConfig, LOCAL_AGENT_CONTAINER_NAME import docker @@ -11,8 +11,11 @@ ignore_ssl_and_certificate_errors = configs.get('ignore_ssl_and_certificate_errors') -@when('the agent container is started') -def run_local_agent_container(context): +@step('the agent container is started on port {port}') +def run_local_agent_container(context, port): + if port.isdigit(): + port = int(port) + assert_that(port, any_of(equal_to('default'), instance_of(int)), "Unexpected value for port") orb_address = configs.get('orb_address') interface = configs.get('orb_agent_interface', 'mock') agent_docker_image = configs.get('agent_docker_image', 'ns1labs/orb-agent') @@ -21,12 +24,18 @@ def run_local_agent_container(context): env_vars = {"ORB_CLOUD_ADDRESS": orb_address, "ORB_CLOUD_MQTT_ID": context.agent['id'], "ORB_CLOUD_MQTT_CHANNEL_ID": context.agent['channel_id'], - "ORB_CLOUD_MQTT_KEY": context.agent['key'], + "ORB_CLOUD_MQTT_KEY": context.agent_key, "PKTVISOR_PCAP_IFACE_DEFAULT": interface} if ignore_ssl_and_certificate_errors == 'true': env_vars["ORB_TLS_VERIFY"] = "false" + if port == "default": + port = str(10853) + else: + env_vars["ORB_BACKENDS_PKTVISOR_API_PORT"] = str(port) - context.container_id = run_agent_container(agent_image, env_vars) + context.container_id = run_agent_container(agent_image, env_vars, LOCAL_AGENT_CONTAINER_NAME) + if port not in context.containers_id.keys(): + context.containers_id[str(port)] = context.container_id @then('the container logs should contain the message "{text_to_match}" within {time_to_wait} seconds') @@ -47,23 +56,48 @@ def check_agent_log(context, text_to_match, time_to_wait): assert_that(text_found, is_(True), 'Message "' + text_to_match + '" was not found in the agent logs!') -@when("the agent container is started using the command provided by the UI") -def run_container_using_ui_command(context): +@then("container on port {port} is {status} after {seconds} seconds") +def check_container_on_port_status(context, port, status, seconds): + if port.isdigit(): + port = int(port) + assert_that(port, any_of(equal_to('default'), instance_of(int)), "Unexpected value for port") + if port == "default": + port = str(10853) + time.sleep(int(seconds)) + check_container_status(context.containers_id[str(port)], status) + + +@step("last container created is {status} after {seconds} seconds") +def check_last_container_status(context, status, seconds): + time.sleep(int(seconds)) + check_container_status(context.container_id, status) + + +@step("the agent container is started using the command provided by the UI on port {port}") +def run_container_using_ui_command(context, port): + if port.isdigit(): + port = int(port) + assert_that(port, any_of(equal_to('default'), instance_of(int)), "Unexpected value for port") context.container_id = run_local_agent_from_terminal(context.agent_provisioning_command, - ignore_ssl_and_certificate_errors) + ignore_ssl_and_certificate_errors, str(port)) assert_that(context.container_id, is_not((none()))) rename_container(context.container_id, LOCAL_AGENT_CONTAINER_NAME) + if port == "default": + port = str(10853) + if port not in context.containers_id.keys(): + context.containers_id[str(port)] = context.container_id -def run_agent_container(container_image, env_vars): +def run_agent_container(container_image, env_vars, container_name): """ Gets a specific agent from Orb control plane :param (str) container_image: that will be used for running the container :param (dict) env_vars: that will be passed to the container context + :param (str) container_name: base of container name :returns: (str) the container ID """ - + LOCAL_AGENT_CONTAINER_NAME = container_name + random_string(5) client = docker.from_env() container = client.containers.run(container_image, name=LOCAL_AGENT_CONTAINER_NAME, detach=True, network_mode='host', environment=env_vars) @@ -101,16 +135,20 @@ def check_logs_contain_message(logs, expected_message): return False -def run_local_agent_from_terminal(command, ignore_ssl_and_certificate_errors): +def run_local_agent_from_terminal(command, ignore_ssl_and_certificate_errors, pktvisor_port): """ :param (str) command: docker command to provision an agent :param (bool) ignore_ssl_and_certificate_errors: True if orb address doesn't have a valid certificate. + :param (str or int) pktvisor_port: Port on which pktvisor should run :return: agent container ID """ args = shlex.split(command) if ignore_ssl_and_certificate_errors == 'true': args.insert(-1, "-e") args.insert(-1, "ORB_TLS_VERIFY=false") + if pktvisor_port != 'default': + args.insert(-1, "-e") + args.insert(-1, f"ORB_BACKENDS_PKTVISOR_API_PORT={pktvisor_port}") terminal_running = subprocess.Popen( args, stdout=subprocess.PIPE) subprocess_return = terminal_running.stdout.read().decode() @@ -123,8 +161,22 @@ def rename_container(container_id, container_name): """ :param container_id: agent container ID - :param container_name: agent container name + :param container_name: base of agent container name """ + container_name = container_name + random_string(5) rename_container_command = f"docker rename {container_id} {container_name}" rename_container_args = shlex.split(rename_container_command) subprocess.Popen(rename_container_args, stdout=subprocess.PIPE) + + +def check_container_status(container_id, status): + """ + + :param container_id: agent container ID + :param status: status that we expect to find in the container + """ + docker_client = docker.from_env() + container = docker_client.containers.list(all=True, filters={'id': container_id}) + assert_that(container, has_length(1)) + container = container[0] + assert_that(container.status, equal_to(status), f"Container {container_id} failed with status {container.status}") diff --git a/python-test/features/steps/login.py b/python-test/features/steps/login.py index f4c795594..ab6e55cca 100644 --- a/python-test/features/steps/login.py +++ b/python-test/features/steps/login.py @@ -18,7 +18,36 @@ def check_registered_account(context): authenticate(email, password) -@when('request referred account registration using registered email, {password_status} password, {username} user name and {company} company name') +@when("user request account registration {email} email, {password} password, {username} user name and {company} " + "company name") +def check_account_input(context, email, password, username, company): + inputs = {'email': email, 'password': password, 'username': username, 'company': company} + for key, value in inputs.items(): + assert_that(value, any_of(equal_to('with'), equal_to('without')), + f"Not expected option to {key}") + account_input = {'email': None, 'password': None, 'company': None, 'username': None, 'reg_status': 201, + 'auth_status': 201} + if email == "without" or password == "without": + account_input['reg_status'] = 400 + account_input['auth_status'] = 400 + elif email == "with": + account_input['email'] = f"test_email_{random_string(3)}@email.com" + if password == "without": + account_input['auth_status'] = 403 + elif password == "with": + account_input['password'] = configs.get('password') + if username == "with": + account_input['username'] = f"test_user {random_string(3)}" + if company == "with": + account_input['company'] = f"test_company {random_string(3)}" + + register_account(account_input['email'], account_input['password'], account_input['company'], + account_input['username'], account_input['reg_status']) + context.auth_response = authenticate(account_input['email'], account_input['password'], account_input['auth_status']) + + +@when('request referred account registration using registered email, {password_status} password, {username} user name ' + 'and {company} company name') def request_account_registration(context, password_status, username, company): assert_that(password_status, any_of(equal_to('registered'), equal_to('unregistered')), "Not expected option to password") @@ -66,4 +95,3 @@ def request_orb_authentication(context, email_status, password_status): def check_access_denied(context): assert_that(context.auth_response, not_(has_key("token"))) assert_that(context.auth_response.keys(), only_contains("error")) - diff --git a/python-test/features/steps/login_ui.py b/python-test/features/steps/login_ui.py index b4e9025a7..9f9520c42 100644 --- a/python-test/features/steps/login_ui.py +++ b/python-test/features/steps/login_ui.py @@ -1,5 +1,5 @@ from users import authenticate -from behave import given, when, then +from behave import given, when, then, step from test_config import TestConfig from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC @@ -18,10 +18,10 @@ def logs_in_orb_ui(context): orb_page(context) use_credentials(context) check_home_page(context) - context.token = authenticate(user_email, user_password) + context.token = authenticate(user_email, user_password)['token'] -@given("that the user is on the orb page") +@step("that the user is on the orb page") def orb_page(context): current_url = go_to_page(base_orb_url, context) assert_that(current_url, equal_to(f"{base_orb_url}/auth/login"), "user not enabled to access orb login page") diff --git a/python-test/features/steps/page_objects.py b/python-test/features/steps/page_objects.py new file mode 100644 index 000000000..8bf4d8faf --- /dev/null +++ b/python-test/features/steps/page_objects.py @@ -0,0 +1,71 @@ +#XPATHs + +class LeftMenu: + def __init__(self): + pass + + @classmethod + def fleet_management(cls): + return "//a[contains(@title, 'Fleet Management')]" + + @classmethod + def agents(cls): + return "//a[contains(@title, 'Agents')]" + + +class AgentsPage: + def __init__(self): + pass + + @classmethod + def new_agent_button(cls): + return "//button[contains(text(), 'New Agent')]" + + @classmethod + def agent_name(cls): + return "//input[contains(@data-orb-qa-id, 'input#name')]" + + @classmethod + def agent_tag_key(cls): + return "//input[contains(@data-orb-qa-id, 'input#orb_tag_key')]" + + @classmethod + def agent_tag_value(cls): + return "//input[contains(@data-orb-qa-id, 'input#orb_tag_value')]" + + @classmethod + def agent_add_tag_button(cls): + return "//button[contains(@data-orb-qa-id, 'button#addTag')]" + + @classmethod + def agent_key(cls): + return "//label[contains(text(), 'Agent Key')]/following::pre[1]" + + @classmethod + def agent_provisioning_command(cls): + return "//label[contains(text(), 'Provisioning Command')]/following::pre[1]" + + @classmethod + def agent_view_id(cls): + return "//label[contains(text(), 'Agent ID')]/following::p" + + @classmethod + def agent_status(cls): + return "//label[contains(text(), 'Health Status')]/following::p" + + +class UtilButton: + def __init__(self): + pass + + @classmethod + def next_button(cls): + return "//button[contains(text(), 'Next')]" + + @classmethod + def save_button(cls): + return "//button[contains(text(), 'Save')]" + + @classmethod + def close_button(cls): + return "//span[contains(@class, 'nb-close')]" diff --git a/python-test/features/steps/users.py b/python-test/features/steps/users.py index abf0f1967..3dddcc6a4 100644 --- a/python-test/features/steps/users.py +++ b/python-test/features/steps/users.py @@ -13,6 +13,7 @@ def register_orb_account(context): password = configs.get('password') if configs.get('is_credentials_registered') == 'false': register_account(email, password) + configs['is_credentials_registered'] = 'true' authenticate(email, password) @@ -36,8 +37,10 @@ def authenticate(user_email, user_password, expected_status_code=201): """ headers = {'Content-type': 'application/json', 'Accept': '*/*'} + json_request = {'email': user_email, 'password': user_password} + json_request = {parameter: value for parameter, value in json_request.items() if value} response = requests.post(base_orb_url + '/api/v1/tokens', - json={'email': user_email, 'password': user_password}, + json=json_request, headers=headers) assert_that(response.status_code, equal_to(expected_status_code), 'Authentication failed with status= ' + str(response.status_code)) diff --git a/python-test/features/steps/utils.py b/python-test/features/steps/utils.py index 71735f7a4..0a40b89ea 100644 --- a/python-test/features/steps/utils.py +++ b/python-test/features/steps/utils.py @@ -1,6 +1,9 @@ import random import string from json import loads, JSONDecodeError +from hamcrest import * + +tag_prefix = "test_tag_" def random_string(k=10): @@ -49,3 +52,54 @@ def insert_str(str_base, str_to_insert, index): :return: (str) string with letter inserted on determined index """ return str_base[:index] + str_to_insert + str_base[index:] + + +def generate_random_string_with_predefined_prefix(string_prefix, n_random=10): + """ + :param (str) string_prefix: prefix to identify object created by tests + :param (int) n_random: amount of random characters + :return: random_string_with_predefined_prefix + """ + random_string_with_predefined_prefix = string_prefix + random_string(n_random) + return random_string_with_predefined_prefix + + +def create_tags_set(orb_tags): + """ + Create a set of orb-tags + :param orb_tags: If defined: the defined tags that should compose the set. + If random: the number of tags that the set must contain. + :return: (dict) tag_set + """ + tag_set = dict() + if orb_tags.isdigit() is False: + assert_that(orb_tags, matches_regexp("^.+\:.+"), "Unexpected tags") + for tag in orb_tags.split(", "): + key, value = tag.split(":") + tag_set[key] = value + else: + amount_of_tags = int(orb_tags.split()[0]) + for tag in range(amount_of_tags): + tag_set[tag_prefix + random_string(4)] = tag_prefix + random_string(2) + return tag_set + + +def check_logs_contain_message_and_name(logs, expected_message, name, name_key): + """ + Gets the logs from Orb agent container + + :param (list) logs: list of log lines + :param (str) expected_message: message that we expect to find in the logs + :param (str) name: element name that we expect to find in the logs + :param (str) name_key: key to get element name on log line + :returns: (bool) whether expected message was found in the logs + """ + + for log_line in logs: + log_line = safe_load_json(log_line) + + if log_line is not None and log_line['msg'] == expected_message: + if log_line is not None and log_line[name_key] == name: + return True, log_line + + return False, "Logs doesn't contain the message and name expected" diff --git a/python-test/mkdocs.yml b/python-test/mkdocs.yml new file mode 100644 index 000000000..5c8af50af --- /dev/null +++ b/python-test/mkdocs.yml @@ -0,0 +1,232 @@ +site_name: ORB TESTS +theme: + name: material + favicon: img/ORB-logo-ring.png + logo: img/ORB-logo-ring.png +use_directory_urls: false +nav: + - Development guide: development_guide.md + + - Scenarios: + - Scenarios list: index.md + - Login Scenarios: + - Request registration of a registered account using registered password username and company: login/request_registration_of_a_registered_account_using_registered_password_username_and_company.md + - Request registration of a registered account using registered password and username: login/request_registration_of_a_registered_account_using_registered_password_and_username.md + - Request registration of a registered account using registered password and company: login/request_registration_of_a_registered_account_using_registered_password_and_company.md + - Request registration of a registered account using registered password: login/request_registration_of_a_registered_account_using_registered_password.md + - Request registration of a registered account using unregistered password username and company: login/request_registration_of_a_registered_account_using_unregistered_password_username_and_company.md + - Request registration of a registered account using unregistered password and username: login/request_registration_of_a_registered_account_using_unregistered_password_and_username.md + - Request registration of a registered account using unregistered password and company: login/request_registration_of_a_registered_account_using_unregistered_password_and_company.md + - Request registration of a registered account using unregistered password: login/request_registration_of_a_registered_account_using_unregistered_password.md + - Request registration of an unregistered account with valid password and invalid email: login/request_registration_of_an_unregistered_account_with_valid_password_and_invalid_email.md + - Request registration of an unregistered account with valid password and valid email: login/request_registration_of_an_unregistered_account_with_valid_password_and_valid_email.md + - Request registration of an unregistered account with invalid password and valid email: login/request_registration_of_an_unregistered_account_with_invalid_password_and_valid_email.md + - Request registration of an unregistered account with invalid password and invalid email: login/request_registration_of_an_unregistered_account_with_invalid_password_and_invalid_email.md + - Check if email and password are required fields: login/check_if_email_and_password_are_required_fields.md + - Login with valid credentials: login/login_with_valid_credentials.md + - Login with invalid credentials: login/login_with_invalid_credentials.md + - Request password with registered email address: login/request_password_with_registered_email_address.md + - Request password with unregistered email address: login/request_password_with_unregistered_email_address.md + - Agents Scenarios: + - Check if total agent on agents' page is correct: agents/check_if_total_agent_on_agents'_page_is_correct.md + - Create agent without tags: agents/create_agent_without_tags.md + - Create agent with one tag: agents/create_agent_with_one_tag.md + - Create agent with multiple tags: agents/create_agent_with_multiple_tags.md + - Create agent with invalid name (regex): agents/create_agent_with_invalid_name_(regex).md + - Create agent with duplicate name: agents/create_agent_with_duplicate_name.md + - Test agent filters: agents/test_agent_filters.md + - Check agent details: agents/check_agent_details.md + - Edit an agent through the details modal: agents/edit_an_agent_through_the_details_modal.md + - Edit agent name: agents/edit_agent_name.md + - Edit agent tag: agents/edit_agent_tag.md + - Save agent without tag: agents/save_agent_without_tag.md + - Insert tags in agents created without tags: agents/insert_tags_in_agents_created_without_tags.md + - Check if is possible cancel operations with no change: agents/check_if_is_possible_cancel_operations_with_no_change.md + - Remove agent using correct name: agents/remove_agent_using_correct_name.md + - Remove agent using incorrect name: agents/remove_agent_using_incorrect_name.md + - Edit agent name and tag: agents/edit_agent_name_and_tags.md + - Agent Groups Scenarios: + - Check if total agent groups on agent groups' page is correct: agent_groups/check_if_total_agent_groups_on_agent_groups'_page_is_correct.md + - Create agent group with invalid name (regex): agent_groups/create_agent_group_with_invalid_name_(regex).md + - Create agent group with duplicate name: agent_groups/create_agent_group_with_duplicate_name.md + - Create agent group with description: agent_groups/create_agent_group_with_description.md + - Create agent group without description: agent_groups/create_agent_group_without_description.md + - Create agent group without tag: agent_groups/create_agent_group_without_tag.md + - Create agent group with one tag: agent_groups/create_agent_group_with_one_tag.md + - Create agent group with multiple tags: agent_groups/create_agent_group_with_multiple_tags.md + - Test agent groups filters: agent_groups/test_agent_groups_filters.md + - Visualize matching agents: agent_groups/visualize_matching_agents.md + - Check agent groups details: agent_groups/check_agent_groups_details.md + - Edit an agent group through the details modal: agent_groups/edit_an_agent_group_through_the_details_modal.md + - Check if is possible cancel operations with no change: agent_groups/check_if_is_possible_cancel_operations_with_no_change.md + - Edit agent group name: agent_groups/edit_agent_group_name.md + - Edit agent group description: agent_groups/edit_agent_group_description.md + - Edit agent group tag: agent_groups/edit_agent_group_tag.md + - Remove agent group using correct name: agent_groups/remove_agent_group_using_correct_name.md + - Remove agent group using incorrect name: agent_groups/remove_agent_group_using_incorrect_name.md + - Run two orb agents on the same port: agents/run_two_orb_agents_on_the_same_port.md + - Run two orb agents on different ports: agents/run_two_orb_agents_on_different_ports.md + - Sink Scenarios: + - Check if total sinks on sinks' page is correct: sinks/check_if_total_sinks_on_sinks'_page_is_correct.md + - Create sink with invalid name (regex): sinks/create_sink_with_invalid_name_(regex).md + - Create sink with duplicate name: sinks/create_sink_with_duplicate_name.md + - Create sink with description: sinks/create_sink_with_description.md + - Create sink without description: sinks/create_sink_without_description.md + - Create sink without tags: sinks/create_sink_without_tags.md + - Create sink with tags: sinks/create_sink_with_tags.md + - Create sink with multiple tags: sinks/create_sink_with_multiple_tags.md + - Check if remote host, username and password are required to create a sink: sinks/check_if_remote_host,_username_and_password_are_required_to_create_a_sink.md + - Test sink filters: sinks/test_sink_filters.md + - Check sink details: sinks/check_sink_details.md + - Edit a sink through the details modal: sinks/edit_a_sink_through_the_details_modal.md + - Edit sink name: sinks/edit_sink_name.md + - Edit sink description: sinks/edit_sink_description.md + - Edit sink remote host: sinks/edit_sink_remote_host.md + - Edit sink username: sinks/edit_sink_username.md + - Edit sink password: sinks/edit_sink_password.md + - Edit sink tags: sinks/edit_sink_tags.md + - Check if is possible cancel operations with no change: sinks/check_if_is_possible_cancel_operations_with_no_change.md + - Remove sink using correct name: sinks/remove_sink_using_correct_name.md + - Remove sink using incorrect name: sinks/remove_sink_using_incorrect_name.md + - Policies Scenarios: + - Check if total policies on policies' page is correct: policies/check_if_total_policies_on_policies'_page_is_correct.md + - Create policy with invalid name (regex): policies/create_policy_with_invalid_name_(regex).md + - Create policy with no agent provisioned: policies/create_policy_with_no_agent_provisioned.md + - Create policy with duplicate name: policies/create_policy_with_duplicate_name.md + - Create policy with description: policies/create_policy_with_description.md + - Create policy without description: policies/create_policy_without_description.md + - Create policy with dhcp handler: policies/create_policy_with_dhcp_handler.md + - Create policy with dns handler: policies/create_policy_with_dns_handler.md + - Create policy with net handler: policies/create_policy_with_net_handler.md + - Create policy with multiple handlers: policies/create_policy_with_multiple_handlers.md + - Test policy filters: policies/test_policy_filters.md + - Check policies details: policies/check_policies_details.md + - Edit a policy through the details modal: policies/edit_a_policy_through_the_details_modal.md + - Edit policy name: policies/edit_policy_name.md + - Edit policy description: policies/edit_policy_description.md + - Edit policy handler: policies/edit_policy_handler.md + - Check if is possible cancel operations with no change: policies/check_if_is_possible_cancel_operations_with_no_change.md + - Remove policy using correct name: policies/remove_policy_using_correct_name.md + - Remove policy using incorrect name: policies/remove_policy_using_incorrect_name.md + - Datasets Scenarios: + - Check if total datasets on datasets' page is correct: datasets/check_if_total_datasets_on_datasets'_page_is_correct.md + - Create dataset with invalid name (regex): datasets/create_dataset_with_invalid_name_(regex).md + - Create dataset: datasets/create_dataset.md + - Check datasets details: datasets/check_datasets_details.md + - Check if is possible cancel operations with no change: datasets/check_if_is_possible_cancel_operations_with_no_change.md + - Test datasets filter: datasets/test_datasets_filter.md + - Edit a dataset through the details modal: datasets/edit_a_dataset_through_the_details_modal.md + - Edit dataset name: datasets/edit_dataset_name.md + - Edit dataset sink: datasets/edit_dataset_sink.md + - Remove dataset using correct name: datasets/remove_dataset_using_correct_name.md + - Remove dataset using incorrect name: datasets/remove_dataset_using_incorrect_name.md + - Integration Scenarios: + - Check if sink is active while scraping metrics: integration/sink_active_while_scraping_metrics.md + - Check if sink with invalid credentials becomes active: integration/sink_error_invalid_credentials.md + - Check if after 30 minutes without data sink becomes idle: integration/sink_idle_30_minutes.md + - Provision agent before group (check if agent subscribes to the group): integration/provision_agent_before_group.md + - Provision agent after group (check if agent subscribes to the group): integration/provision_agent_after_group.md + - Provision agent with tag matching existing group linked to a valid dataset: integration/multiple_agents_subscribed_to_a_group.md + - Apply multiple policies to a group: integration/apply_multiple_policies.md + - Apply multiple policies to a group and remove one policy: integration/remove_one_of_multiple_policies.md + - Apply multiple policies to a group and remove all of them: integration/remove_all_policies.md + - Apply multiple policies to a group and remove one dataset: integration/remove_one_of_multiple_datasets.md + - Apply multiple policies to a group and remove all datasets: integration/remove_all_datasets.md + - Apply the same policy twice to the agent: integration/apply_policy_twice.md + - Delete sink linked to a dataset, create another one and edit dataset using new sink: integration/change_sink_on_dataset.md + - Remove one of multiples datasets that apply the same policy to the agent: integration/remove_one_dataset_of_multiples_with_same_policy.md + - Remove group (invalid dataset, agent logs): integration/remove_group.md + - Remove sink (invalid dataset, agent logs): integration/remove_sink.md + - Remove policy (invalid dataset, agent logs, heartbeat): integration/remove_policy.md + - Remove dataset (check agent logs, heartbeat): integration/remove_dataset.md + - Remove agent container (logs, agent groups matches): integration/remove_agent_container.md + - Remove agent container force (logs, agent groups matches): integration/remove_agent_container_force.md + - Remove agent (logs, agent groups matches): integration/remove_agent.md + - Subscribe an agent to multiple groups created before agent provisioning: integration/subscribe_an_agent_to_multiple_groups_created_before_agent_provisioning.md + - Subscribe an agent to multiple groups created after agent provisioning: integration/subscribe_an_agent_to_multiple_groups_created_after_agent_provisioning.md + - Agent subscription to group after editing agent's tags: integration/agent_subscription_to_group_after_editing_agent's_tags.md + - Agent subscription to group with policies after editing agent's tags: integration/agent_subscription_to_group_with_policies_after_editing_agent's_tags.md + - Edit agent name and apply policies to then: integration/edit_agent_name_and_apply_policies_to_then.md + - Insert tags in agents created without tags and apply policies to group matching new tags.md: integration/insert_tags_in_agents_created_without_tags_and_apply_policies_to_group_matching_new_tags.md + + - Suites: + - Smoke: + - Smoke list: smoke.md + - Smoke tests: + - Request registration of a registered account using registered password username and company: login/request_registration_of_a_registered_account_using_registered_password_username_and_company.md + - Request registration of a registered account using registered password and username: login/request_registration_of_a_registered_account_using_registered_password_and_username.md + - Request registration of a registered account using registered password and company: login/request_registration_of_a_registered_account_using_registered_password_and_company.md + - Request registration of a registered account using registered password: login/request_registration_of_a_registered_account_using_registered_password.md + - Request registration of a registered account using unregistered password username and company: login/request_registration_of_a_registered_account_using_unregistered_password_username_and_company.md + - Request registration of a registered account using unregistered password and username: login/request_registration_of_a_registered_account_using_unregistered_password_and_username.md + - Request registration of a registered account using unregistered password and company: login/request_registration_of_a_registered_account_using_unregistered_password_and_company.md + - Request registration of a registered account using unregistered password: login/request_registration_of_a_registered_account_using_unregistered_password.md + - Request registration of an unregistered account with valid password and invalid email: login/request_registration_of_an_unregistered_account_with_valid_password_and_invalid_email.md + - Request registration of an unregistered account with valid password and valid email: login/request_registration_of_an_unregistered_account_with_valid_password_and_valid_email.md + - Request registration of an unregistered account with invalid password and valid email: login/request_registration_of_an_unregistered_account_with_invalid_password_and_valid_email.md + - Request registration of an unregistered account with invalid password and invalid email: login/request_registration_of_an_unregistered_account_with_invalid_password_and_invalid_email.md + - Check if email and password are required fields: login/check_if_email_and_password_are_required_fields.md + - Login with valid credentials: login/login_with_valid_credentials.md + - Login with invalid credentials: login/login_with_invalid_credentials.md + - Request password with registered email address: login/request_password_with_registered_email_address.md + - Request password with unregistered email address: login/request_password_with_unregistered_email_address.md + - Check if sink is active while scraping metrics: integration/sink_active_while_scraping_metrics.md + - Provision agent before group (check if agent subscribes to the group): integration/provision_agent_before_group.md + - Provision agent after group (check if agent subscribes to the group): integration/provision_agent_after_group.md + - Provision agent with tag matching existing group linked to a valid dataset: integration/multiple_agents_subscribed_to_a_group.md + - Apply multiple policies to a group: integration/apply_multiple_policies.md + - Apply multiple policies to a group and remove one policy: integration/remove_one_of_multiple_policies.md + - Apply multiple policies to a group and remove one dataset: integration/remove_one_of_multiple_datasets.md + - Apply the same policy twice to the agent: integration/apply_policy_twice.md + - Remove group (invalid dataset, agent logs): integration/remove_group.md + - Remove sink (invalid dataset, agent logs): integration/remove_sink.md + - Remove dataset (check agent logs, heartbeat): integration/remove_dataset.md + - Remove agent container (logs, agent groups matches): integration/remove_agent_container.md + - Remove agent container force (logs, agent groups matches): integration/remove_agent_container_force.md + - Remove agent (logs, agent groups matches): integration/remove_agent.md + - Run two orb agents on the same port: agents/run_two_orb_agents_on_the_same_port.md + - Run two orb agents on different ports: agents/run_two_orb_agents_on_different_ports.md + - Subscribe an agent to multiple groups created before agent provisioning: integration/subscribe_an_agent_to_multiple_groups_created_before_agent_provisioning.md + - Subscribe an agent to multiple groups created after agent provisioning: integration/subscribe_an_agent_to_multiple_groups_created_after_agent_provisioning.md + - Agent subscription to group after editing agent's tags: integration/agent_subscription_to_group_after_editing_agent's_tags.md + - Agent subscription to group with policies after editing agent's tags: integration/agent_subscription_to_group_with_policies_after_editing_agent's_tags.md + - Edit agent name and apply policies to then: integration/edit_agent_name_and_apply_policies_to_then.md + - Insert tags in agents created without tags and apply policies to group matching new tags.md: integration/insert_tags_in_agents_created_without_tags_and_apply_policies_to_group_matching_new_tags.md + - Sanity: + - Sanity list: sanity.md + - Sanity tests: + - Request registration of a registered account using registered password username and company: login/request_registration_of_a_registered_account_using_registered_password_username_and_company.md + - Request registration of a registered account using registered password and username: login/request_registration_of_a_registered_account_using_registered_password_and_username.md + - Request registration of a registered account using registered password and company: login/request_registration_of_a_registered_account_using_registered_password_and_company.md + - Request registration of a registered account using registered password: login/request_registration_of_a_registered_account_using_registered_password.md + - Request registration of a registered account using unregistered password username and company: login/request_registration_of_a_registered_account_using_unregistered_password_username_and_company.md + - Request registration of a registered account using unregistered password and username: login/request_registration_of_a_registered_account_using_unregistered_password_and_username.md + - Request registration of a registered account using unregistered password and company: login/request_registration_of_a_registered_account_using_unregistered_password_and_company.md + - Request registration of a registered account using unregistered password: login/request_registration_of_a_registered_account_using_unregistered_password.md + - Request registration of an unregistered account with valid password and invalid email: login/request_registration_of_an_unregistered_account_with_valid_password_and_invalid_email.md + - Request registration of an unregistered account with valid password and valid email: login/request_registration_of_an_unregistered_account_with_valid_password_and_valid_email.md + - Request registration of an unregistered account with invalid password and valid email: login/request_registration_of_an_unregistered_account_with_invalid_password_and_valid_email.md + - Request registration of an unregistered account with invalid password and invalid email: login/request_registration_of_an_unregistered_account_with_invalid_password_and_invalid_email.md + - Check if sink is active while scraping metrics: integration/sink_active_while_scraping_metrics.md + - Provision agent before group (check if agent subscribes to the group): integration/provision_agent_before_group.md + - Provision agent after group (check if agent subscribes to the group): integration/provision_agent_after_group.md + - Provision agent with tag matching existing group linked to a valid dataset: integration/multiple_agents_subscribed_to_a_group.md + - Apply multiple policies to a group: integration/apply_multiple_policies.md + - Apply multiple policies to a group and remove one policy: integration/remove_one_of_multiple_policies.md + - Apply multiple policies to a group and remove one dataset: integration/remove_one_of_multiple_datasets.md + - Apply the same policy twice to the agent: integration/apply_policy_twice.md + - Remove group (invalid dataset, agent logs): integration/remove_group.md + - Remove sink (invalid dataset, agent logs): integration/remove_sink.md + - Remove dataset (check agent logs, heartbeat): integration/remove_dataset.md + - Remove agent container (logs, agent groups matches): integration/remove_agent_container.md + - Remove agent container force (logs, agent groups matches): integration/remove_agent_container_force.md + - Remove agent (logs, agent groups matches): integration/remove_agent.md + - Run two orb agents on the same port: agents/run_two_orb_agents_on_the_same_port.md + - Run two orb agents on different ports: agents/run_two_orb_agents_on_different_ports.md + - Subscribe an agent to multiple groups created before agent provisioning: integration/subscribe_an_agent_to_multiple_groups_created_before_agent_provisioning.md + - Subscribe an agent to multiple groups created after agent provisioning: integration/subscribe_an_agent_to_multiple_groups_created_after_agent_provisioning.md + - Agent subscription to group after editing agent's tags: integration/agent_subscription_to_group_after_editing_agent's_tags.md + - Agent subscription to group with policies after editing agent's tags: integration/agent_subscription_to_group_with_policies_after_editing_agent's_tags.md + - Edit agent name and tag: agents/edit_agent_name_and_tags.md + - Edit agent name and apply policies to then: integration/edit_agent_name_and_apply_policies_to_then.md + - Insert tags in agents created without tags and apply policies to group matching new tags.md: integration/insert_tags_in_agents_created_without_tags_and_apply_policies_to_group_matching_new_tags.md diff --git a/sinker/backend/pktvisor/pktvisor.go b/sinker/backend/pktvisor/pktvisor.go index 4d850bc3a..a2da7f0c9 100644 --- a/sinker/backend/pktvisor/pktvisor.go +++ b/sinker/backend/pktvisor/pktvisor.go @@ -11,6 +11,7 @@ import ( "github.com/mitchellh/mapstructure" "github.com/ns1labs/orb/fleet" "github.com/ns1labs/orb/fleet/pb" + "github.com/ns1labs/orb/pkg/errors" "github.com/ns1labs/orb/sinker/backend" "github.com/ns1labs/orb/sinker/prometheus" "go.uber.org/zap" @@ -72,6 +73,12 @@ func (p pktvisorBackend) ProcessMetrics(agent *pb.OwnerRes, agentID string, data p.logger.Error("error decoding packets handler", zap.Error(err)) continue } + } else if data, ok := handlerData["dhcp"]; ok { + err := mapstructure.Decode(data, &stats.DHCP) + if err != nil { + p.logger.Error("error decoding dhcp handler", zap.Error(err)) + continue + } } } return parseToProm(&context, stats), nil @@ -84,24 +91,24 @@ func parseToProm(ctxt *context, stats StatSnapshot) prometheus.TSList { return tsList } -func convertToPromParticle(ctxt *context, m map[string]interface{}, label string, tsList *prometheus.TSList) { - for k, v := range m { - switch c := v.(type) { +func convertToPromParticle(ctxt *context, statsMap map[string]interface{}, label string, tsList *prometheus.TSList) { + for key, value := range statsMap { + switch statistic := value.(type) { case map[string]interface{}: // Call convertToPromParticle recursively until the last interface of the StatSnapshot struct - // The prom particle label it's been formed during the recursive call - convertToPromParticle(ctxt, c, label+k, tsList) + // The prom particle label it's been formed during the recursive call (concatenation) + convertToPromParticle(ctxt, statistic, label+key, tsList) // The StatSnapshot has two ways to record metrics (i.e. Live int64 `mapstructure:"live"`) // It's why we check if the type is int64 case int64: { // Use this regex to identify if the value it's a quantile var matchFirstQuantile = regexp.MustCompile("^([P-p])+[0-9]") - if ok := matchFirstQuantile.MatchString(k); ok { + if ok := matchFirstQuantile.MatchString(key); ok { // If it's quantile, needs to be parsed to prom quantile format - tsList = makePromParticle(ctxt, label, k, v, tsList, ok) + tsList = makePromParticle(ctxt, label, key, value, tsList, ok, "") } else { - tsList = makePromParticle(ctxt, label+k, "", v, tsList, false) + tsList = makePromParticle(ctxt, label+key, "", value, tsList, false, "") } } // The StatSnapshot has two ways to record metrics (i.e. TopIpv4 []NameCount `mapstructure:"top_ipv4"`) @@ -109,78 +116,92 @@ func convertToPromParticle(ctxt *context, m map[string]interface{}, label string // Here we extract the value for Name and Estimate case []interface{}: { - for _, value := range c { + for _, value := range statistic { m, ok := value.(map[string]interface{}) if !ok { return } - var lbl string - var dtpt interface{} + var promLabel string + var promDataPoint interface{} for k, v := range m { switch k { case "Name": { - lbl = fmt.Sprintf("%v", v) + promLabel = fmt.Sprintf("%v", v) } case "Estimate": { - dtpt = v + promDataPoint = v } } } - tsList = makePromParticle(ctxt, label+k, lbl, dtpt, tsList, false) + tsList = makePromParticle(ctxt, label+key, promLabel, promDataPoint, tsList, false, key) } } } } } -func makePromParticle(ctxt *context, label string, k string, v interface{}, tsList *prometheus.TSList, quantile bool) *prometheus.TSList { - mapQuantiles := make(map[string]float64) - mapQuantiles["P50"] = 0.50 - mapQuantiles["P90"] = 0.90 - mapQuantiles["P95"] = 0.95 - mapQuantiles["P99"] = 0.99 +func makePromParticle(ctxt *context, label string, k string, v interface{}, tsList *prometheus.TSList, quantile bool, name string) *prometheus.TSList { + mapQuantiles := make(map[string]string) + mapQuantiles["P50"] = "0.5" + mapQuantiles["P90"] = "0.9" + mapQuantiles["P95"] = "0.95" + mapQuantiles["P99"] = "0.99" var dpFlag dp var labelsListFlag labelList if err := labelsListFlag.Set(fmt.Sprintf("__name__;%s", camelToSnake(label))); err != nil { handleParticleError(ctxt, err) + return tsList } if err := labelsListFlag.Set("instance;" + ctxt.agent.AgentName); err != nil { handleParticleError(ctxt, err) + return tsList } if err := labelsListFlag.Set("agent_id;" + ctxt.agentID); err != nil { handleParticleError(ctxt, err) + return tsList } if err := labelsListFlag.Set("agent;" + ctxt.agent.AgentName); err != nil { handleParticleError(ctxt, err) + return tsList } if err := labelsListFlag.Set("policy_id;" + ctxt.policyID); err != nil { handleParticleError(ctxt, err) + return tsList } if err := labelsListFlag.Set("policy;" + ctxt.policyName); err != nil { handleParticleError(ctxt, err) + return tsList } if k != "" { if quantile { if value, ok := mapQuantiles[k]; ok { - if err := labelsListFlag.Set(fmt.Sprintf("quantile;%.2f", value)); err != nil { + if err := labelsListFlag.Set(fmt.Sprintf("quantile;%s", value)); err != nil { handleParticleError(ctxt, err) + return tsList } } } else { - if err := labelsListFlag.Set(fmt.Sprintf("name;%s", k)); err != nil { + parsedName, err := topNMetricsParser(name) + if err != nil { + ctxt.logger.Error("failed to parse Top N metric, default value it'll be used", zap.Error(err)) + parsedName = "name" + } + if err := labelsListFlag.Set(fmt.Sprintf("%s;%s", parsedName, k)); err != nil { handleParticleError(ctxt, err) + return tsList } } } if err := dpFlag.Set(fmt.Sprintf("now,%d", v)); err != nil { handleParticleError(ctxt, err) + return tsList } *tsList = append(*tsList, prometheus.TimeSeries{ - Labels: []prometheus.Label(labelsListFlag), + Labels: labelsListFlag, Datapoint: prometheus.Datapoint(dpFlag), }) return tsList @@ -201,6 +222,9 @@ func camelToSnake(s string) string { var strExcept = "" if len(sub) > 1 { strExcept = matchExcept.FindAllString(s, 1)[0] + if strExcept == "pASN" { + strExcept = "p_ASN" + } s = sub[0] } @@ -210,6 +234,28 @@ func camelToSnake(s string) string { return lower + strExcept } +func topNMetricsParser(label string) (string, error) { + mapNMetrics := make(map[string]string) + mapNMetrics["TopGeoLoc"] = "geo_loc" + mapNMetrics["TopASN"] = "asn" + mapNMetrics["TopIpv6"] = "ipv6" + mapNMetrics["TopIpv4"] = "ipv4" + mapNMetrics["TopQname2"] = "qname" + mapNMetrics["TopQname3"] = "qname" + mapNMetrics["TopNxdomain"] = "qname" + mapNMetrics["TopQtype"] = "qtype" + mapNMetrics["TopRcode"] = "rcode" + mapNMetrics["TopREFUSED"] = "qname" + mapNMetrics["TopSRVFAIL"] = "qname" + mapNMetrics["TopUDPPorts"] = "port" + mapNMetrics["TopSlow"] = "qname" + if value, ok := mapNMetrics[label]; ok { + return value, nil + } else { + return "", errors.New(fmt.Sprintf("top N metric not mapped for parse: %s", label)) + } +} + func Register(logger *zap.Logger) bool { backend.Register("pktvisor", &pktvisorBackend{logger: logger}) return true diff --git a/sinker/backend/pktvisor/pktvisor_test.go b/sinker/backend/pktvisor/pktvisor_test.go new file mode 100644 index 000000000..c73555862 --- /dev/null +++ b/sinker/backend/pktvisor/pktvisor_test.go @@ -0,0 +1,321 @@ +package pktvisor_test + +import ( + "fmt" + "github.com/gofrs/uuid" + "github.com/ns1labs/orb/fleet" + "github.com/ns1labs/orb/fleet/pb" + "github.com/ns1labs/orb/sinker/backend" + "github.com/ns1labs/orb/sinker/backend/pktvisor" + "github.com/ns1labs/orb/sinker/prometheus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "reflect" + "testing" +) + +func TestDHCPConversion(t *testing.T) { + var logger *zap.Logger + pktvisor.Register(logger) + + ownerID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + policyID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + agentID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + var agent = &pb.OwnerRes{ + OwnerID: ownerID.String(), + AgentName: "agent-test", + } + + data := fleet.AgentMetricsRPCPayload{ + PolicyID: policyID.String(), + PolicyName: "policy-test", + Datasets: nil, + Format: "json", + BEVersion: "1.0", + } + + be := backend.GetBackend("pktvisor") + + cases := map[string]struct { + data []byte + expected prometheus.TimeSeries + }{ + "DHCPPayloadWirePacketsFiltered": { + data: []byte(` +{ + "policy_dhcp": { + "dhcp": { + "wire_packets": { + "filtered": 10 + } + } + } +}`), + expected: prometheus.TimeSeries{ + Labels: []prometheus.Label{ + { + Name: "__name__", + Value: "dhcp_wire_packets_filtered", + }, + { + Name: "instance", + Value: "agent-test", + }, + { + Name: "agent_id", + Value: agentID.String(), + }, + { + Name: "agent", + Value: "agent-test", + }, + { + Name: "policy_id", + Value: policyID.String(), + }, + { + Name: "policy", + Value: "policy-test", + }, + }, + Datapoint: prometheus.Datapoint{ + Value: 10, + }, + }, + }, + } + + for desc, c := range cases { + t.Run(desc, func(t *testing.T) { + data.Data = c.data + res, err := be.ProcessMetrics(agent, agentID.String(), data) + require.Nil(t, err, fmt.Sprintf("%s: unexpected error: %s", desc, err)) + var receivedLabel []prometheus.Label + var receivedDatapoint prometheus.Datapoint + for _, value := range res { + if c.expected.Labels[0] == value.Labels[0] { + receivedLabel = value.Labels + receivedDatapoint = value.Datapoint + } + } + assert.True(t, reflect.DeepEqual(c.expected.Labels, receivedLabel), fmt.Sprintf("%s: expected %v got %v", desc, c.expected.Labels, receivedLabel)) + assert.Equal(t, c.expected.Datapoint.Value, receivedDatapoint.Value, fmt.Sprintf("%s: expected value %f got %f", desc, c.expected.Datapoint.Value, receivedDatapoint.Value)) + }) + } + +} + +func TestASNConversion(t *testing.T) { + var logger *zap.Logger + pktvisor.Register(logger) + + ownerID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + policyID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + agentID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + var agent = &pb.OwnerRes{ + OwnerID: ownerID.String(), + AgentName: "agent-test", + } + + data := fleet.AgentMetricsRPCPayload{ + PolicyID: policyID.String(), + PolicyName: "policy-test", + Datasets: nil, + Format: "json", + BEVersion: "1.0", + } + + be := backend.GetBackend("pktvisor") + + cases := map[string]struct { + data []byte + expected prometheus.TimeSeries + }{ + "PacketPayloadTopASN": { + data: []byte(` +{ + "policy_packets": { + "packets": { + "top_ASN": [ + { + "estimate": 996, + "name": "36236/NETACTUATE" + } + ] + } + } +}`), + expected: prometheus.TimeSeries{ + Labels: []prometheus.Label{ + { + Name: "__name__", + Value: "packets_top_ASN", + }, + { + Name: "instance", + Value: "agent-test", + }, + { + Name: "agent_id", + Value: agentID.String(), + }, + { + Name: "agent", + Value: "agent-test", + }, + { + Name: "policy_id", + Value: policyID.String(), + }, + { + Name: "policy", + Value: "policy-test", + }, + { + Name: "asn", + Value: "36236/NETACTUATE", + }, + }, + Datapoint: prometheus.Datapoint{ + Value: 996, + }, + }, + }, + } + + for desc, c := range cases { + t.Run(desc, func(t *testing.T) { + data.Data = c.data + res, err := be.ProcessMetrics(agent, agentID.String(), data) + require.Nil(t, err, fmt.Sprintf("%s: unexpected error: %s", desc, err)) + var receivedLabel []prometheus.Label + var receivedDatapoint prometheus.Datapoint + for _, value := range res { + if c.expected.Labels[0] == value.Labels[0] { + receivedLabel = value.Labels + receivedDatapoint = value.Datapoint + } + } + assert.True(t, reflect.DeepEqual(c.expected.Labels, receivedLabel), fmt.Sprintf("%s: expected %v got %v", desc, c.expected.Labels, receivedLabel)) + assert.Equal(t, c.expected.Datapoint.Value, receivedDatapoint.Value, fmt.Sprintf("%s: expected value %f got %f", desc, c.expected.Datapoint.Value, receivedDatapoint.Value)) + }) + } + +} + +func TestGeoLocConversion(t *testing.T) { + var logger *zap.Logger + pktvisor.Register(logger) + + ownerID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + policyID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + agentID, err := uuid.NewV4() + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + var agent = &pb.OwnerRes{ + OwnerID: ownerID.String(), + AgentName: "agent-test", + } + + data := fleet.AgentMetricsRPCPayload{ + PolicyID: policyID.String(), + PolicyName: "policy-test", + Datasets: nil, + Format: "json", + BEVersion: "1.0", + } + + be := backend.GetBackend("pktvisor") + + cases := map[string]struct { + data []byte + expected prometheus.TimeSeries + }{ + "PacketPayloadTopASN": { + data: []byte(` +{ + "policy_packets": { + "packets": { + "top_geoLoc": [ + { + "estimate": 996, + "name": "AS/Hong Kong/HCW/Central" + } + ] + } + } +}`), + expected: prometheus.TimeSeries{ + Labels: []prometheus.Label{ + { + Name: "__name__", + Value: "packets_top_geoLoc", + }, + { + Name: "instance", + Value: "agent-test", + }, + { + Name: "agent_id", + Value: agentID.String(), + }, + { + Name: "agent", + Value: "agent-test", + }, + { + Name: "policy_id", + Value: policyID.String(), + }, + { + Name: "policy", + Value: "policy-test", + }, + { + Name: "geo_loc", + Value: "AS/Hong Kong/HCW/Central", + }, + }, + Datapoint: prometheus.Datapoint{ + Value: 996, + }, + }, + }, + } + + for desc, c := range cases { + t.Run(desc, func(t *testing.T) { + data.Data = c.data + res, err := be.ProcessMetrics(agent, agentID.String(), data) + require.Nil(t, err, fmt.Sprintf("%s: unexpected error: %s", desc, err)) + var receivedLabel []prometheus.Label + var receivedDatapoint prometheus.Datapoint + for _, value := range res { + if c.expected.Labels[0] == value.Labels[0] { + receivedLabel = value.Labels + receivedDatapoint = value.Datapoint + } + } + assert.True(t, reflect.DeepEqual(c.expected.Labels, receivedLabel), fmt.Sprintf("%s: expected %v got %v", desc, c.expected.Labels, receivedLabel)) + assert.Equal(t, c.expected.Datapoint.Value, receivedDatapoint.Value, fmt.Sprintf("%s: expected value %f got %f", desc, c.expected.Datapoint.Value, receivedDatapoint.Value)) + }) + } + +} diff --git a/sinker/backend/pktvisor/types.go b/sinker/backend/pktvisor/types.go index 1ff4aabd6..e5ff92095 100644 --- a/sinker/backend/pktvisor/types.go +++ b/sinker/backend/pktvisor/types.go @@ -161,3 +161,4 @@ type StatSnapshot struct { Packets PacketPayload Pcap PcapPayload } + diff --git a/sinks/backend/prometheus/prometheus.go b/sinks/backend/prometheus/prometheus.go index d3bc2c34f..4a3de9b2d 100644 --- a/sinks/backend/prometheus/prometheus.go +++ b/sinks/backend/prometheus/prometheus.go @@ -48,7 +48,7 @@ func (p *prometheusBackend) CreateFeatureConfig() []backend.ConfigFeature { remoteHost := backend.ConfigFeature{ Type: "text", Input: "text", - Title: "Remote Host", + Title: "Remote Write URL", Name: "remote_host", Required: true, } diff --git a/ui/package.json b/ui/package.json index 99192357c..25f6f9aed 100644 --- a/ui/package.json +++ b/ui/package.json @@ -95,7 +95,7 @@ "@types/d3-color": "1.0.5", "@types/jasmine": "2.5.54", "@types/jasminewd2": "2.0.3", - "@types/node": "^12.11.1", + "@types/node": "12.20.42", "axios": "^0.21.1", "body-parser": "^1.19.0", "codelyzer": "^5.1.2", diff --git a/ui/src/app/common/interfaces/orb/agent.policy.interface.ts b/ui/src/app/common/interfaces/orb/agent.policy.interface.ts index 4c0ee12f4..f42705e17 100644 --- a/ui/src/app/common/interfaces/orb/agent.policy.interface.ts +++ b/ui/src/app/common/interfaces/orb/agent.policy.interface.ts @@ -30,6 +30,11 @@ export interface AgentPolicy extends OrbEntity { */ version?: number; + /** + * Schema Version + */ + schema_version?: string; + /** * A timestamp of creation {string} */ diff --git a/ui/src/app/common/interfaces/orb/policy/config/policy.config.interface.ts b/ui/src/app/common/interfaces/orb/policy/config/policy.config.interface.ts index d068a71cd..7a11afcf4 100644 --- a/ui/src/app/common/interfaces/orb/policy/config/policy.config.interface.ts +++ b/ui/src/app/common/interfaces/orb/policy/config/policy.config.interface.ts @@ -6,6 +6,7 @@ */ import { PolicyTap } from 'app/common/interfaces/orb/policy/policy.tap.interface'; +import { PolicyHandler } from 'app/common/interfaces/orb/policy/policy.handler.interface'; /** * @interface PolicyConfig @@ -31,11 +32,7 @@ export interface PolicyConfig { */ handlers?: { modules?: { - [propName: string]: { - config: { [propName: string]: any }, - type: string, - [propName: string]: any, - } | string | any, + [propName: string]: PolicyHandler | string | any, }, }; } diff --git a/ui/src/app/common/interfaces/orb/policy/policy.backend.interface.ts b/ui/src/app/common/interfaces/orb/policy/policy.backend.interface.ts new file mode 100644 index 000000000..153124152 --- /dev/null +++ b/ui/src/app/common/interfaces/orb/policy/policy.backend.interface.ts @@ -0,0 +1,26 @@ +/** + * Agent Policy / Backend Interface + * + * [Policies Architecture]{@link https://github.com/ns1labs/orb/wiki/Architecture:-Policies-and-Datasets} + */ + +/** + * @interface PolicyBackend + */ +export interface PolicyBackend { + /** + * backend denomination {string} + */ + backend?: string; + + /** + * description {string} + */ + description?: string; + + /** + * schema version {string} + */ + schema_version?: string; +} + diff --git a/ui/src/app/common/interfaces/orb/policy/policy.handler.interface.ts b/ui/src/app/common/interfaces/orb/policy/policy.handler.interface.ts new file mode 100644 index 000000000..7ab4a309d --- /dev/null +++ b/ui/src/app/common/interfaces/orb/policy/policy.handler.interface.ts @@ -0,0 +1,51 @@ +/** + * Agent Policy / Handler Module Interface + * + * [Policies Architecture]{@link https://github.com/ns1labs/orb/wiki/Architecture:-Policies-and-Datasets} + */ + +/** + * @interface PolicyHandler + */ +export interface PolicyHandler { + /** + * name {string} + */ + name?: string; + + /** + * version {string} + */ + version?: string; + + /** + * type {string} + */ + type?: string; + + /** + * config {} + */ + config?: { [propName: string]: {} | any }; + + /** + * filter {} + */ + filter?: { [propName: string]: {} | any }; + + /** + * metrics {} + */ + metrics?: { [propName: string]: {} | any }; + + /** + * metrics_groups {} + */ + metrics_groups?: { [propName: string]: {} | any }; + + /** + * content + */ + content?: { [propName: string]: {} | any }; +} + diff --git a/ui/src/app/common/interfaces/orb/policy/policy.input.interface.ts b/ui/src/app/common/interfaces/orb/policy/policy.input.interface.ts new file mode 100644 index 000000000..6674da857 --- /dev/null +++ b/ui/src/app/common/interfaces/orb/policy/policy.input.interface.ts @@ -0,0 +1,24 @@ +/** + * Agent Policy / Input Config Interface + * + * [Policies Architecture]{@link https://github.com/ns1labs/orb/wiki/Architecture:-Policies-and-Datasets} + * [Agent Taps](https://github.com/ns1labs/pktvisor/blob/develop/RFCs/2021-04-16-75-taps.md) + */ + +/** + * @interface PolicyInput + */ +export interface PolicyInput { + version?: string; + + /** + * json object with configs + */ + config?: { [propName: string]: any }; + + /** + * json object with configs + */ + filter?: { [propName: string]: any }; +} + diff --git a/ui/src/app/pages/datasets/add/dataset.add.component.html b/ui/src/app/pages/datasets/add/dataset.add.component.html index ec0608813..6fdccf363 100644 --- a/ui/src/app/pages/datasets/add/dataset.add.component.html +++ b/ui/src/app/pages/datasets/add/dataset.add.component.html @@ -39,22 +39,23 @@

{{isEdit ? 'Edit Dataset' : 'New Dataset'}}


-
@@ -91,22 +92,31 @@

{{isEdit ? 'Edit Dataset' : 'New Dataset'}}


- +
@@ -142,22 +152,30 @@

{{isEdit ? 'Edit Dataset' : 'New Dataset'}}


- +
@@ -218,23 +236,30 @@

{{isEdit ? 'Edit Dataset' : 'New Dataset'}}


- +
@@ -285,20 +310,29 @@

{{isEdit ? 'Edit Dataset' : 'New Dataset'}}


- +
diff --git a/ui/src/app/pages/datasets/add/dataset.add.component.ts b/ui/src/app/pages/datasets/add/dataset.add.component.ts index 60d701b52..4d83325ff 100644 --- a/ui/src/app/pages/datasets/add/dataset.add.component.ts +++ b/ui/src/app/pages/datasets/add/dataset.add.component.ts @@ -12,6 +12,7 @@ import { OrbPagination } from 'app/common/interfaces/orb/pagination.interface'; import { AgentGroup } from 'app/common/interfaces/orb/agent.group.interface'; import { AgentPolicy } from 'app/common/interfaces/orb/agent.policy.interface'; import { Sink } from 'app/common/interfaces/orb/sink.interface'; +import { STRINGS } from '../../../../assets/text/strings'; const CONFIG = { SINKS: 'SINKS', @@ -26,6 +27,9 @@ const CONFIG = { styleUrls: ['./dataset.add.component.scss'], }) export class DatasetAddComponent { + // page vars + strings = {stepper: STRINGS.stepper}; + // stepper form groups detailsFormGroup: FormGroup; diff --git a/ui/src/app/pages/datasets/policies.agent/add/agent.policy.add.component.html b/ui/src/app/pages/datasets/policies.agent/add/agent.policy.add.component.html index 67b0f6f23..d1f4c0064 100644 --- a/ui/src/app/pages/datasets/policies.agent/add/agent.policy.add.component.html +++ b/ui/src/app/pages/datasets/policies.agent/add/agent.policy.add.component.html @@ -52,22 +52,23 @@

{{ isEdit ? 'Edit Agent Policy' : 'Create Agent Policy'}}


-
@@ -88,7 +89,6 @@

{{ isEdit ? 'Edit Agent Policy' : 'Create Agent Policy'}}

* {{ isEdit ? 'Edit Agent Policy' : 'Create Agent Policy'}} {{ option.key }} {{' | input type: ' + option.value?.input_type }} + class="faded-input-text"> {{' | input type: ' + option.value?.input_type }} @@ -140,7 +140,7 @@

{{ isEdit ? 'Edit Agent Policy' : 'Create Agent Policy'}}

[type]="control.value.type" nbTooltip="{{ control.value.description }}"> {{ isEdit ? 'Edit Agent Policy' : 'Create Agent Policy'}} [type]="control.value.type" nbTooltip="{{ control.value.description }}"> {{ isEdit ? 'Edit Agent Policy' : 'Create Agent Policy'}} [type]="control.value.type" nbTooltip="{{ control.value.description }}"> {{ isEdit ? 'Edit Agent Policy' : 'Create Agent Policy'}}
- +
@@ -294,8 +301,6 @@

{{ isEdit ? 'Edit Agent Policy' : 'Create Agent Policy'}}

Setup any number of handlers

-
@@ -313,190 +318,41 @@

{{ isEdit ? 'Edit Agent Policy' : 'Create Agent Policy'}}

-
-
- - * -
- - - {{ handler.key }} - -
-
-
- - * -
- - -
-
-
- -
-
-
-
-
- -
-
- - - - {{ control.value.name + "-" + control.value.props.options }} - {{ option.value }} - - - {{control.value.name}} - -
-
-
-
-
-
- -
-
-
-
-
- -
-
- - - - {{ control.value.name + "-" + control.value.props.options }} - {{ option.value }} - - - {{control.value.name}} - -
-
-
-
-
-
+
diff --git a/ui/src/app/pages/datasets/policies.agent/add/agent.policy.add.component.scss b/ui/src/app/pages/datasets/policies.agent/add/agent.policy.add.component.scss index 431f17027..110b2abb6 100644 --- a/ui/src/app/pages/datasets/policies.agent/add/agent.policy.add.component.scss +++ b/ui/src/app/pages/datasets/policies.agent/add/agent.policy.add.component.scss @@ -120,7 +120,7 @@ mat-chip nb-icon { } } -.tap-input-text { +.faded-input-text { color: #969fb9; font-family: 'Montserrat', sans-serif; padding-left: 5px; diff --git a/ui/src/app/pages/datasets/policies.agent/add/agent.policy.add.component.ts b/ui/src/app/pages/datasets/policies.agent/add/agent.policy.add.component.ts index 54a2e62b9..0f75c4e16 100644 --- a/ui/src/app/pages/datasets/policies.agent/add/agent.policy.add.component.ts +++ b/ui/src/app/pages/datasets/policies.agent/add/agent.policy.add.component.ts @@ -7,6 +7,9 @@ import { AgentPolicy } from 'app/common/interfaces/orb/agent.policy.interface'; import { DynamicFormConfig } from 'app/common/interfaces/orb/dynamic.form.interface'; import { AgentPoliciesService } from 'app/common/services/agents/agent.policies.service'; import { PolicyTap } from 'app/common/interfaces/orb/policy/policy.tap.interface'; +import { NbDialogService } from '@nebular/theme'; +import { HandlerPolicyAddComponent } from 'app/pages/datasets/policies.agent/add/handler.policy.add.component'; +import { STRINGS } from '../../../../../assets/text/strings'; const CONFIG = { TAPS: 'TAPS', @@ -22,6 +25,9 @@ const CONFIG = { styleUrls: ['./agent.policy.add.component.scss'], }) export class AgentPolicyAddComponent { + // page vars + strings = {stepper: STRINGS.stepper}; + // #forms // agent policy general information - name, desc, backend detailsFG: FormGroup; @@ -35,12 +41,6 @@ export class AgentPolicyAddComponent { // dynamic input filter config inputFilterFG: FormGroup; - // handlers - handlerSelectorFG: FormGroup; - - dynamicHandlerConfigFG: FormGroup; - dynamicHandlerFilterFG: FormGroup; - // #key inputs holders // selected backend object backend: { [propName: string]: any }; @@ -55,15 +55,6 @@ export class AgentPolicyAddComponent { filter?: DynamicFormConfig, }; - // holds selected handler conf. - // handler template currently selected, to be edited by user and then added to the handlers list or discarded - liveHandler: { - version?: string, - config?: DynamicFormConfig, - filter?: DynamicFormConfig, - type?: string, - }; - // holds all handlers added by user modules: { [propName: string]: { @@ -93,17 +84,6 @@ export class AgentPolicyAddComponent { }, }; - availableHandlers: { - [propName: string]: { - version?: string, - config?: DynamicFormConfig, - filter?: DynamicFormConfig, - metrics?: DynamicFormConfig, - metrics_groups?: DynamicFormConfig, - }, - } = {}; - - // #if edit agentPolicy: AgentPolicy; agentPolicyID: string; @@ -115,7 +95,7 @@ export class AgentPolicyAddComponent { .reduce((acc, [value]) => { acc[value] = false; return acc; - }, {}); + }, {}) as { [propName: string]: boolean }; constructor( private agentPoliciesService: AgentPoliciesService, @@ -123,6 +103,7 @@ export class AgentPolicyAddComponent { private router: Router, private route: ActivatedRoute, private _formBuilder: FormBuilder, + private dialogService: NbDialogService, ) { this.agentPolicyID = this.route.snapshot.paramMap.get('id'); this.agentPolicy = this.newAgent(); @@ -169,6 +150,11 @@ export class AgentPolicyAddComponent { }); } + isLoadComplete() { + return !Object.values(this.isLoading).reduce((prev, curr) => prev || curr, false); + } + + readyForms() { const { name, @@ -196,14 +182,6 @@ export class AgentPolicyAddComponent { selected_tap: [tap, Validators.required], input_type: [input_type, Validators.required], }); - - this.handlerSelectorFG = this._formBuilder.group({ - 'selected_handler': ['', [Validators.required]], - 'label': ['', [Validators.required]], - }); - - this.dynamicHandlerConfigFG = this._formBuilder.group({}); - this.dynamicHandlerFilterFG = this._formBuilder.group({}); } updateForms() { @@ -222,12 +200,7 @@ export class AgentPolicyAddComponent { this.modules = modules; - this.dynamicHandlerConfigFG = this._formBuilder.group({}); - this.dynamicHandlerFilterFG = this._formBuilder.group({}); - this.onBackendSelected(backend).catch(reason => console.warn(`${ reason }`)); - - } getBackendsList() { @@ -259,7 +232,7 @@ export class AgentPolicyAddComponent { } getBackendData() { - return Promise.all([this.getTaps(), this.getInputs(), this.getHandlers()]) + return Promise.all([this.getTaps(), this.getInputs()]) .then(value => { if (this.isEdit && this.agentPolicy) { const selected_tap = this.agentPolicy.policy.input.tap; @@ -342,7 +315,7 @@ export class AgentPolicyAddComponent { // if editing, some values might not be overrideable any longer, all should be prefilled in form const { config: agentConfig, filter: agentFilter } = this.agentPolicy.policy.input; // tap config values, cannot be overridden if set - const {config_predefined: preConfig, filter_predefined: preFilter} = this.tap; + const { config_predefined: preConfig, filter_predefined: preFilter } = this.tap; // populate form controls for config const inputConfDynamicCtrl = Object.entries(inputConfig) @@ -376,99 +349,39 @@ export class AgentPolicyAddComponent { } - getHandlers() { - return new Promise((resolve) => { - this.isLoading[CONFIG.HANDLERS] = true; - - this.agentPoliciesService.getBackendConfig([this.backend.backend, 'handlers']) - .subscribe(handlers => { - this.availableHandlers = handlers || {}; - - this.isLoading[CONFIG.HANDLERS] = false; - resolve(handlers); - }); + addHandler() { + this.dialogService.open(HandlerPolicyAddComponent, { + context: { + backend: this.backend, + modules: this.modules, + }, + autoFocus: true, + closeOnEsc: true, + }).onClose.subscribe((handler) => { + // save handler to the policy being created/edited + if (handler) { + this.onHandlerAdded(handler); + } }); } - onHandlerSelected(selectedHandler) { - if (this.dynamicHandlerConfigFG) { - this.dynamicHandlerConfigFG = null; - } - if (this.dynamicHandlerFilterFG) { - this.dynamicHandlerFilterFG = null; - } - - // TODO - hardcoded for v: 1.0 -: always retrieve latest - this.liveHandler = selectedHandler !== '' && !!this.availableHandlers[selectedHandler] ? - { ...this.availableHandlers[selectedHandler]['1.0'], type: selectedHandler } : null; - - const { config, filter } = this.liveHandler || { config: {}, filter: {} }; - - const dynamicConfigControls = Object.entries(config || {}).reduce((controls, [key]) => { - controls[key] = ['', [Validators.required]]; - return controls; - }, {}); - - this.dynamicHandlerConfigFG = Object.keys(dynamicConfigControls).length > 0 ? this._formBuilder.group(dynamicConfigControls) : null; - - const dynamicFilterControls = Object.entries(filter || {}).reduce((controls, [key]) => { - controls[key] = ['', [Validators.required]]; - return controls; - }, {}); - - const suggestName = this.getSeriesHandlerName(selectedHandler); - this.handlerSelectorFG.patchValue({label: suggestName}); - - this.dynamicHandlerFilterFG = Object.keys(dynamicFilterControls).length > 0 ? this._formBuilder.group(dynamicFilterControls) : null; - } - - getSeriesHandlerName(handlerType) { - const count = 1 + Object.values(this.modules || {}).filter(({type}) => type === handlerType).length; - return `handler_${handlerType}_${count}`; - } + onHandlerAdded(handler) { + const { config, filter, type, name } = handler; - checkValidName() { - const { value } = this.handlerSelectorFG.controls.label; - const hasTagForKey = Object.keys(this.modules).find(key => key === value); - return value && value !== '' && !hasTagForKey; - } - - onHandlerAdded() { - let config = {}; - let filter = {}; - - if (this.dynamicHandlerConfigFG !== null) { - config = Object.entries(this.dynamicHandlerConfigFG.controls) - .reduce((acc, [key, control]) => { - if (control.value) acc[key] = control.value; - return acc; - }, {}); - } - - if (this.dynamicHandlerFilterFG !== null) { - filter = Object.entries(this.dynamicHandlerFilterFG.controls) - .reduce((acc, [key, control]) => { - if (control.value) acc[key] = control.value; - return acc; - }, {}); - } - - const handlerName = this.handlerSelectorFG.controls.label.value; - this.modules[handlerName] = ({ - type: this.liveHandler.type, + this.modules[name] = ({ + type, config, filter, }); - this.handlerSelectorFG.reset({ - selected_handler: { value: '', disabled: false }, - label: { value: '', disabled: false }, - }); - this.onHandlerSelected(''); } - onHandlerRemoved(handlerName) { - delete this.modules[handlerName]; + onHandlerRemoved(name) { + delete this.modules[name]; + } + + hasModules() { + return Object.keys(this.modules).length > 0; } goBack() { @@ -494,7 +407,7 @@ export class AgentPolicyAddComponent { if (!!value && value !== '') acc.config[key] = value; } return acc; - }, {config: {}}), + }, { config: {} }), ...Object.entries(this.inputFilterFG.controls) .map(([key, control]) => ({ [key]: control.value })) .reduce((acc, curr) => { @@ -502,15 +415,15 @@ export class AgentPolicyAddComponent { if (!!value && value !== '') acc.filter[key] = value; } return acc; - }, {filter: {}}), + }, { filter: {} }), }, handlers: { modules: Object.entries(this.modules).reduce((acc, [key, value]) => { - const {type, config, filter} = value; + const { type, config, filter } = value; acc[key] = { - type: type, - config: Object.entries(config).length > 0 && config || undefined, - filter: Object.entries(filter).length > 0 && filter || undefined, + type, + config, + filter, }; if (Object.keys(config || {}).length > 0) acc[key][config] = config; return acc; diff --git a/ui/src/app/pages/datasets/policies.agent/add/handler.policy.add.component.html b/ui/src/app/pages/datasets/policies.agent/add/handler.policy.add.component.html new file mode 100644 index 000000000..61add7521 --- /dev/null +++ b/ui/src/app/pages/datasets/policies.agent/add/handler.policy.add.component.html @@ -0,0 +1,123 @@ + + + Handler Configuration + + + +
+ + * + + + {{ handler.key }} + {{ ' | ' + handler.value.version }} + +
+
+
+ + * +
+ +
+
+
+
+ +
+
+ + + + {{ control.value.name + "-" + control.value.props.options }} + {{ option.value }} + + + {{control.value.name}} + +
+
+
+
+
+
+
+ + + + +
+ diff --git a/ui/src/app/pages/datasets/policies.agent/add/handler.policy.add.component.scss b/ui/src/app/pages/datasets/policies.agent/add/handler.policy.add.component.scss new file mode 100644 index 000000000..38fb858c9 --- /dev/null +++ b/ui/src/app/pages/datasets/policies.agent/add/handler.policy.add.component.scss @@ -0,0 +1,118 @@ +.nb-card-large { + width: 800px; +} + +.nb-card-medium { + width: 497px; +} + +.nb-card-small { + width: 280px; +} + +nb-icon { + vertical-align: middle; +} + +.form-key { + color: #969fb9; +} + +.p { + color: #ffffff; + text-align: left; +} + +.ns1red { + color: #df316f; +} + +nb-select { + width: 100%; +} + +button { + float: right; +} + +textarea { + width: 100%; + height: 100%; +} + +nb-tabset { + padding: 0; + margin: 0; +} + +// ORB +.sink-edit-button { + background-color: blue; +} + +.orb-close-dialog { + background-color: #23294000; + border-radius: 4px; + display: contents; + float: right; + + > span { + float: right; + padding-top: 0.3rem; + font-size: large; + color: #3089fc; + font-weight: 900; + } +} + +.row { + display: flex; + flex-wrap: wrap; + margin-right: 32px; + margin-left: 6px; + margin-top: 15px; + margin-bottom: 15px; + + div > p { + margin-top: 0; + margin-bottom: 0.4rem; + } +} + +.detail-title { + color: #969fb9; +} + +nb-accordion { + padding: 0; + text-subtitle-line-height: 1rem; + display: grid; + + nb-accordion-item { + padding: 0; + //background: rgb(23, 28, 48); + + nb-accordion-item-header { + padding: 10px 0; + //background: rgb(23, 28, 48); + } + + nb-accordion-item-body { + padding: 0; + //background: rgb(23, 28, 48); + display: grid; + } + } +} + +.faded-input-text { + color: #969fb9; + font-family: 'Montserrat', sans-serif; + padding-left: 5px; + font-weight: 300; +} + +.required { + color: #df316f; + padding-left: 2px; +} diff --git a/ui/src/app/pages/datasets/policies.agent/add/handler.policy.add.component.ts b/ui/src/app/pages/datasets/policies.agent/add/handler.policy.add.component.ts new file mode 100644 index 000000000..6ce133219 --- /dev/null +++ b/ui/src/app/pages/datasets/policies.agent/add/handler.policy.add.component.ts @@ -0,0 +1,185 @@ +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { NbDialogRef } from '@nebular/theme'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { AgentPoliciesService } from 'app/common/services/agents/agent.policies.service'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { DynamicFormConfig } from 'app/common/interfaces/orb/dynamic.form.interface'; +import { PolicyHandler } from 'app/common/interfaces/orb/policy/policy.handler.interface'; +import { PolicyBackend } from 'app/common/interfaces/orb/policy/policy.backend.interface'; + +type ConfigHandler = PolicyHandler & { + config?: DynamicFormConfig, + filter?: DynamicFormConfig, + metrics?: DynamicFormConfig, + metrics_groups?: DynamicFormConfig, +}; + +interface HandlerCollection { + [propName: string]: ConfigHandler; +} + +interface ModuleCollection { + [propName: string]: PolicyHandler; +} + +@Component({ + selector: 'ngx-agent-policy-details-component', + templateUrl: './handler.policy.add.component.html', + styleUrls: ['./handler.policy.add.component.scss'], +}) +export class HandlerPolicyAddComponent implements OnInit, OnDestroy { + // handlers + handlerSelectorFG: FormGroup; + + // backend - selected by user on agent policy creation + @Input() + backend: PolicyBackend; + + // holds all handlers added by user + @Input() + modules: ModuleCollection = {}; + + // handler key + selectedHandler: string; + + // handler dyn configs to render + dynConfigList = ['config', 'filter']; + + handlerProps: ConfigHandler; + + isLoading: boolean; + + subscription: Subscription; + + availableHandlers: HandlerCollection = {}; + + constructor( + protected dialogRef: NbDialogRef, + protected route: ActivatedRoute, + protected router: Router, + protected _formBuilder: FormBuilder, + protected agentPoliciesService: AgentPoliciesService, + ) { + this.isLoading = true; + } + + ngOnInit() { + this.subscription = this.getHandlers() + .subscribe(handlers => { + this.availableHandlers = handlers; + this.isLoading = false; + }); + + this.readyForms(); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + + onClose() { + this.dialogRef.close(false); + } + + getHandlers() { + return this.agentPoliciesService.getBackendConfig([this.backend.backend, 'handlers']) + .map(handlers => Object.entries<[string, { HandlerCollection }]>(handlers) + .reduce((acc, [key, value]) => { + const latest = Object.entries(value as [string, HandlerCollection]) + .sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0) + .map(([version, content]) => ({ content, version })) + .pop(); + acc[key] = latest; + return acc; + }, {}) + || {}); + } + + readyForms(): void { + this.handlerSelectorFG = this._formBuilder.group({ + 'selected_handler': [null, [Validators.required]], + 'name': [null, [Validators.required]], + 'type': [null], + 'config': [null], + 'filter': [null], + }); + } + + createDynamicControls(config) { + const controlReducer = (previous, [key, __]) => { + previous[key] = ['', [Validators.required]]; + return previous; + }; + + const dynamicControls = Object.entries(config || {}).reduce(controlReducer, {}); + + const dynamicFormGroup = Object.keys(dynamicControls).length > 0 ? this._formBuilder.group(dynamicControls) : null; + + return dynamicFormGroup; + } + + onHandlerSelected(selectedHandler) { + this.selectedHandler = selectedHandler; + + const { config, filter } = this.handlerProps = this.availableHandlers[selectedHandler].content; + + const suggestName = this.getSeriesHandlerName(selectedHandler); + + this.handlerSelectorFG.patchValue({ + name: suggestName, + type: selectedHandler, + }); + + this.handlerSelectorFG.setControl('config', this.createDynamicControls(config)); + this.handlerSelectorFG.setControl('filter', this.createDynamicControls(filter)); + } + + getSeriesHandlerName(handlerType) { + const count = 1 + Object.values(this.modules || {}).filter(({ type }) => type === handlerType).length; + return `handler_${ handlerType }_${ count }`; + } + + checkValidName() { + const { value } = this.handlerSelectorFG.controls.name; + const hasTagForKey = Object.keys(this.modules).find(key => key === value); + return value && value !== '' && !hasTagForKey; + } + + onSaveHandler() { + const configForm = this.handlerSelectorFG.get('config') as FormGroup; + const filterForm = this.handlerSelectorFG.get('filter') as FormGroup; + const { name, type } = this.handlerSelectorFG.value; + let config, filter; + + const valueReducer = (dynConfig) => { + return (acc, [key, control]) => { + if (control.value) { + if (dynConfig[key].type === 'string[]') { + acc[key] = control.value.split(','); // todo we must support separator definition + } else { + acc[key] = control.value; + } + } + return acc; + }; + }; + + if (configForm !== null) { + config = Object.entries(configForm.controls) + .reduce(valueReducer(this.handlerProps['config']), {}); + } + + if (filterForm !== null) { + filter = Object.entries(filterForm.controls) + .reduce(valueReducer(this.handlerProps['filter']), {}); + } + + this.dialogRef.close({ + name, + type, + config, + filter, + }); + } +} diff --git a/ui/src/app/pages/datasets/policies.agent/details/agent.policy.details.component.html b/ui/src/app/pages/datasets/policies.agent/details/agent.policy.details.component.html index e39d26d10..89bfbd9d1 100644 --- a/ui/src/app/pages/datasets/policies.agent/details/agent.policy.details.component.html +++ b/ui/src/app/pages/datasets/policies.agent/details/agent.policy.details.component.html @@ -48,6 +48,6 @@ - + diff --git a/ui/src/app/pages/datasets/policies.agent/details/agent.policy.details.component.ts b/ui/src/app/pages/datasets/policies.agent/details/agent.policy.details.component.ts index f123a8fe6..42a8cacc3 100644 --- a/ui/src/app/pages/datasets/policies.agent/details/agent.policy.details.component.ts +++ b/ui/src/app/pages/datasets/policies.agent/details/agent.policy.details.component.ts @@ -40,7 +40,7 @@ export class AgentPolicyDetailsComponent implements OnInit, OnDestroy { this.subscription.unsubscribe(); } - onOpenEdit(agentPolicy: any) { + onOpenEdit() { this.dialogRef.close(true); } diff --git a/ui/src/app/pages/datasets/policies.agent/list/agent.policy.list.component.ts b/ui/src/app/pages/datasets/policies.agent/list/agent.policy.list.component.ts index 351e29888..291d3521d 100644 --- a/ui/src/app/pages/datasets/policies.agent/list/agent.policy.list.component.ts +++ b/ui/src/app/pages/datasets/policies.agent/list/agent.policy.list.component.ts @@ -19,6 +19,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { Debounce } from 'app/shared/decorators/utils'; import { AgentPolicyDeleteComponent } from 'app/pages/datasets/policies.agent/delete/agent.policy.delete.component'; import { AgentPolicyDetailsComponent } from 'app/pages/datasets/policies.agent/details/agent.policy.details.component'; +import { DatePipe } from '@angular/common'; @Component({ selector: 'ngx-agent-policy-list-component', @@ -66,6 +67,7 @@ export class AgentPolicyListComponent implements OnInit, AfterViewInit, AfterVie constructor( private cdr: ChangeDetectorRef, private dialogService: NbDialogService, + private datePipe: DatePipe, private agentPoliciesService: AgentPoliciesService, private notificationsService: NotificationsService, private route: ActivatedRoute, @@ -113,7 +115,8 @@ export class AgentPolicyListComponent implements OnInit, AfterViewInit, AfterVie minWidth: 60, }, { - prop: 'ts_created', + prop: 'ts_last_modified', + pipe: {transform: (value) => this.datePipe.transform(value, 'MMM d, y, HH:mm:ss z')}, name: 'Last Modified', minWidth: 90, flexGrow: 2, diff --git a/ui/src/app/pages/fleet/agents/add/agent.add.component.html b/ui/src/app/pages/fleet/agents/add/agent.add.component.html index ea69ddba0..0485b95f9 100644 --- a/ui/src/app/pages/fleet/agents/add/agent.add.component.html +++ b/ui/src/app/pages/fleet/agents/add/agent.add.component.html @@ -38,14 +38,13 @@

{{isEdit ? 'Edit Agent' : 'New Agent'}}


-
@@ -68,7 +67,9 @@

{{isEdit ? 'Edit Agent' : 'New Agent'}}

{{strings.add.step.desc2}}

-

Agent tags were optional and may be set here or when you are provisioning an Agent.
The lack of tags will block an Agent to be matched during the Agent Group creation.

+

Agent tags are optional and may be set here or when you are + provisioning an Agent.
The lack of tags will block an Agent to be matched during the Agent Group + creation.

@@ -143,13 +144,20 @@

{{isEdit ? 'Edit Agent' : 'New Agent'}}


- +
@@ -202,13 +210,20 @@

{{isEdit ? 'Edit Agent' : 'New Agent'}}


- +
diff --git a/ui/src/app/pages/fleet/agents/key/agent.key.component.ts b/ui/src/app/pages/fleet/agents/key/agent.key.component.ts index 3cdb2b1f8..82a62e454 100644 --- a/ui/src/app/pages/fleet/agents/key/agent.key.component.ts +++ b/ui/src/app/pages/fleet/agents/key/agent.key.component.ts @@ -43,7 +43,7 @@ export class AgentKeyComponent implements OnInit { -e ORB_CLOUD_MQTT_CHANNEL_ID=${ this.agent.channel_id } \\ -e ORB_CLOUD_MQTT_KEY=${ this.agent.key } \\ -e PKTVISOR_PCAP_IFACE_DEFAULT=mock \\ -ns1labs/orb-agent`; +ns1labs/orb-agent:develop`; this.command2show = `docker run -d --net=host \n -e ORB_CLOUD_ADDRESS=${ document.location.hostname } \n @@ -51,7 +51,7 @@ ns1labs/orb-agent`; -e ORB_CLOUD_MQTT_CHANNEL_ID=${ this.agent.channel_id } \n -e ORB_CLOUD_MQTT_KEY=${ this.agent.key } \n -e PKTVISOR_PCAP_IFACE_DEFAULT=mock \n -ns1labs/orb-agent`; +ns1labs/orb-agent:develop`; } toggleIcon (target) { diff --git a/ui/src/app/pages/fleet/agents/view/agent.view.component.html b/ui/src/app/pages/fleet/agents/view/agent.view.component.html index 845d671f0..5ea13f9af 100644 --- a/ui/src/app/pages/fleet/agents/view/agent.view.component.html +++ b/ui/src/app/pages/fleet/agents/view/agent.view.component.html @@ -24,7 +24,7 @@

Agent View

           
-              
             
@@ -75,10 +76,10 @@

{{strings[isEdit ? 'edit' : 'add']['header']}}

- {{tag | tagchip}} + class="orb-tag-sink " + *ngFor="let tag of selectedTags | keyvalue" + [style.background-color]="tag | tagcolor"> + {{tag | tagchip}} {{strings[isEdit ? 'edit' : 'add']['header']}}
- + -
@@ -201,10 +211,10 @@

{{strings[isEdit ? 'edit' : 'add']['header']}}

- {{tag | tagchip}} + class="orb-tag-sink " + *ngFor="let tag of selectedTags | keyvalue" + [style.background-color]="tag | tagcolor"> + {{tag | tagchip}} {{strings[isEdit ? 'edit' : 'add']['header']}}
+ class="orb w-100" + style="height: 500px;" + [rows]="matchingAgents" + [scrollbarV]="true" + [columns]="columns" + [columnMode]="columnMode.flex" + [headerHeight]="50" + [footerHeight]="50" + [rowHeight]="50">

- + -
@@ -293,10 +312,10 @@

{{strings[isEdit ? 'edit' : 'add']['header']}}

- {{tag | tagchip}} + class="orb-tag-sink " + *ngFor="let tag of value | keyvalue" + [style.background-color]="tag | tagcolor"> + {{tag | tagchip}}
diff --git a/ui/src/app/pages/pages.module.ts b/ui/src/app/pages/pages.module.ts index 917174c69..2139e5a7b 100644 --- a/ui/src/app/pages/pages.module.ts +++ b/ui/src/app/pages/pages.module.ts @@ -59,6 +59,7 @@ import { DatasetListComponent } from 'app/pages/datasets/list/dataset.list.compo import { DatasetDeleteComponent } from 'app/pages/datasets/delete/dataset.delete.component'; import { DatasetAddComponent } from 'app/pages/datasets/add/dataset.add.component'; import { DatasetDetailsComponent } from 'app/pages/datasets/details/dataset.details.component'; +import { HandlerPolicyAddComponent } from 'app/pages/datasets/policies.agent/add/handler.policy.add.component'; @NgModule({ imports: [ @@ -128,6 +129,7 @@ import { DatasetDetailsComponent } from 'app/pages/datasets/details/dataset.deta AgentPolicyDeleteComponent, AgentPolicyDetailsComponent, AgentPolicyListComponent, + HandlerPolicyAddComponent, // Sink Management SinkListComponent, SinkAddComponent, diff --git a/ui/src/app/pages/register/register.component.html b/ui/src/app/pages/register/register.component.html index d3d7b2ab1..0a8a62278 100644 --- a/ui/src/app/pages/register/register.component.html +++ b/ui/src/app/pages/register/register.component.html @@ -45,6 +45,7 @@

Create an account

[required]="getConfigValue('forms.validation.fullName.required')" [minlength]="getConfigValue('forms.validation.fullName.minLength')" [maxlength]="getConfigValue('forms.validation.fullName.maxLength')" + pattern="^([a-zA-Z]+)\s([a-zA-Z ]+)$" [attr.aria-invalid]="fullName.invalid && fullName.touched ? true : null">

@@ -56,6 +57,9 @@

Create an account

to {{getConfigValue('forms.validation.fullName.maxLength')}} characters

+

+ Full name should contain two space separated names +

@@ -187,7 +191,8 @@

Create an account

*ngIf="_isProduction && _psEnabled" accessId="{{_sid}}" groupKey="{{_groupKey}}" - signerIdSelector="input-email"> + signerIdSelector="input-name" + confirmationEmail="1">
@@ -139,22 +140,30 @@

{{strings.sink[isEdit ? 'edit' : 'add']['header']}}


- +
@@ -242,22 +251,29 @@

{{strings.sink[isEdit ? 'edit' : 'add']['header']}}

- +
@@ -323,20 +339,29 @@

{{strings.sink[isEdit ? 'edit' : 'add']['header']}}

- +
diff --git a/ui/src/app/shared/pipes/advanced-options.pipe.ts b/ui/src/app/shared/pipes/advanced-options.pipe.ts index ebd35d10f..934a25d22 100644 --- a/ui/src/app/shared/pipes/advanced-options.pipe.ts +++ b/ui/src/app/shared/pipes/advanced-options.pipe.ts @@ -18,6 +18,10 @@ export class AdvancedOptionsPipe implements PipeTransform { return items; } - return items.filter(item => item.value?.props?.advanced === filter); + return items.filter(item => { + return (!!item.value?.props?.advanced && item.value.props.advanced === filter) + || (filter === false && !item.value?.props?.advanced); + }); + } }