示例#1
0
class FileServerAssociation(DistributedEntityMixin, SoftDeleteMixin, db.Model):
    __tablename__ = 'D_file_server_association'
    order = 30

    file_id = db.Column(typos.UUID, db.ForeignKey('D_file.id'), nullable=False, primary_key=True)
    dst_server_id = db.Column(typos.UUID, db.ForeignKey('D_server.id'), nullable=False, primary_key=True)
    dest_folder = db.Column(db.Text)
    l_mtime = db.Column(db.INTEGER)

    file = db.relationship("File")
    destination_server = db.relationship("Server")

    @property
    def destination_folder(self):
        return self.dest_folder or self.file.dest_folder or os.path.dirname(self.file.target)

    @property
    def target(self):
        return os.path.join(self.destination_folder, os.path.basename(self.file.target))

    def to_json(self, human=False, **kwargs) -> t.Dict:
        data = super().to_json(**kwargs)
        if human:
            data.update({'file': {'target': self.file.target, 'src_server': self.file.source_server.name,
                                  'dst_server': str(self.destination_server.name),
                                  'dest_folder': self.dest_folder}})
        else:
            data.update({'file_id': str(self.file.id), 'dst_server_id': str(self.destination_server.id),
                         'dest_folder': self.dest_folder})
        return data

    @classmethod
    def from_json(cls, kwargs) -> 'FileServerAssociation':
        kwargs = copy.deepcopy(kwargs)
        kwargs['file'] = File.query.get_or_raise(kwargs.get('file_id'))
        kwargs['destination_server'] = Server.query.get_or_raise(kwargs.get('dst_server_id'))
        super().from_json(kwargs)
        try:
            o = cls.query.get((kwargs['file_id'], kwargs['dst_server_id']))
        except RuntimeError as e:
            o = None
        if o:
            for k, v in kwargs.items():
                if getattr(o, k) != v:
                    setattr(o, k, v)
            return o
        else:
            return cls(**kwargs)
示例#2
0
class Service(EntityReprMixin, DistributedEntityMixin, db.Model):
    __tablename__ = 'D_service'
    order = 40

    id = db.Column(UUID, primary_key=True, default=uuid.uuid4)
    name = db.Column(db.String(255), nullable=False)
    details = db.Column(db.JSON)
    created_on = db.Column(UtcDateTime(timezone=True), nullable=False, default=get_now)
    last_ping = db.Column(UtcDateTime(timezone=True))
    status = db.Column(db.String(40))

    orchestrations = db.relationship("Orchestration", secondary="D_service_orchestration",
                                     order_by="ServiceOrchestration.execution_time")

    def __init__(self, name: str, details: Kwargs, status: str, created_on: datetime = get_now(),
                 last_ping: datetime = None, id: uuid.UUID = None, **kwargs):
        super().__init__(**kwargs)
        self.id = id
        self.name = name
        self.details = details
        self.status = status
        self.created_on = created_on
        self.last_ping = last_ping

    def to_json(self):
        return {'id': str(self.id), 'name': self.name, 'details': self.details,
                'last_ping': self.last_ping.strftime("%d/%m/%Y %H:%M:%S"),
                'status': self.status}
示例#3
0
class Vault(DistributedEntityMixin, SoftDeleteMixin, db.Model):
    __tablename__ = 'D_vault'

    user_id = db.Column(UUID, db.ForeignKey('D_user.id'), primary_key=True)
    scope = db.Column(db.String(100), primary_key=True, default='global')
    name = db.Column(db.String(100), primary_key=True)
    _old_name = db.Column(db.String(100))
    value = db.Column(db.PickleType)

    user = db.relationship("User")

    def to_json(self, human=False, **kwargs):
        dto = super().to_json(**kwargs)
        dto.update(name=self.name, value=self.value)
        if self.scope:
            dto.update(scope=self.scope)
        if human:
            dto.update(user={'id': self.user.id, 'name': self.user.name})
        else:
            dto.update(user_id=self.user_id or getattr(self.user, 'id', None))
        return dto

    @classmethod
    def from_json(cls, kwargs):
        super().from_json(kwargs)
        kwargs['user'] = User.query.get_or_raise(kwargs.pop('user_id', None))
        o = cls.query.get(
            (getattr(kwargs.get('user'),
                     'id'), kwargs.get('scope', 'global'), kwargs.get('name')))
        if o:
            for k, v in kwargs.items():
                if getattr(o, k) != v:
                    setattr(o, k, v)
            return o
        else:
            return cls(**kwargs)

    @classmethod
    def get_variables_from(cls, user: t.Union[Id, User], scope='global'):
        if isinstance(user, User):
            user_id = user.id
        else:
            user_id = user
        return {
            vault.name: vault.value
            for vault in cls.query.filter_by(user_id=user_id,
                                             scope=scope).all()
        }

    def __str__(self):
        return f"Vault({self.user}:{self.scope}[{self.name}={self.value}])"
示例#4
0
class Gate(UUIDistributedEntityMixin, SoftDeleteMixin, db.Model):
    __tablename__ = "D_gate"
    order = 2

    server_id = db.Column(UUID)
    dns = db.Column(db.String(100))
    ip = db.Column(IPType)
    port = db.Column(db.Integer, nullable=False)
    hidden = db.Column(db.Boolean, default=False)
    _old_server_id = db.Column(UUID)

    __table_args__ = (db.UniqueConstraint('server_id', 'ip', 'port'),
                      db.UniqueConstraint('server_id', 'dns', 'port'))

    server = db.relationship("Server",
                             backref="gates",
                             foreign_keys=[server_id],
                             primaryjoin="Gate.server_id==Server.id")

    def __init__(self,
                 server: 'Server',
                 port: int = defaults.DEFAULT_PORT,
                 dns: str = None,
                 ip: t.Union[str, ipaddress.IPv4Address,
                             ipaddress.IPv6Address] = None,
                 **kwargs):
        super().__init__(**kwargs)
        self.server = server
        self.port = port
        self.ip = ipaddress.ip_address(ip) if isinstance(ip, str) else ip
        self.dns = dns
        if not (self.dns or self.ip):
            self.dns = server.name
        self.hidden = kwargs.get('hidden', False) or False

    def __str__(self):
        return f'{self.dns or self.ip}:{self.port}'

    def to_json(self, human=False, **kwargs):
        data = super().to_json(**kwargs)
        data.update(server_id=str(self.server.id) if self.server.id else None,
                    ip=str(self.ip) if self.ip else None,
                    dns=self.dns,
                    port=self.port,
                    hidden=self.hidden)
        return data

    @classmethod
    def from_json(cls, kwargs):
        from dimensigon.domain.entities import Server
        server = kwargs.pop('server', None)
        kwargs = copy.deepcopy(kwargs)
        if server:
            kwargs['server'] = server
        kwargs['ip'] = ipaddress.ip_address(kwargs.get('ip')) if isinstance(
            kwargs.get('ip'), str) else kwargs.get('ip')
        if 'server_id' in kwargs and kwargs['server_id'] is not None:
            # through db.session to allow load from removed entities
            kwargs['server'] = db.session.query(Server).filter_by(
                id=kwargs.get('server_id')).one()
            kwargs.pop('server_id')
        return super().from_json(kwargs)
示例#5
0
class Server(UUIDistributedEntityMixin, SoftDeleteMixin, db.Model):
    __tablename__ = 'D_server'
    order = 1

    name = db.Column(db.String(255), nullable=False, unique=True)
    granules = db.Column(ScalarListType())
    _me = db.Column("me", db.Boolean, default=False)
    _old_name = db.Column("$$name", db.String(255))
    l_ignore_on_lock = db.Column(
        "ignore_on_lock", db.Boolean,
        default=False)  # ignore the server for locking when set
    created_on = db.Column(UtcDateTime(timezone=True))  # new in version 3

    route: t.Optional[Route] = db.relationship(
        "Route",
        primaryjoin="Route.destination_id==Server.id",
        uselist=False,
        back_populates="destination",
        cascade="all, delete-orphan")
    gates: t.List[Gate]

    log_sources = db.relationship("Log",
                                  primaryjoin="Server.id==Log.src_server_id",
                                  back_populates="source_server")
    log_destinations = db.relationship(
        "Log",
        primaryjoin="Server.id==Log.dst_server_id",
        back_populates="destination_server")
    files = db.relationship("File", back_populates="source_server")
    file_server_associations = db.relationship(
        "FileServerAssociation", back_populates="destination_server")
    software_server_associations = db.relationship("SoftwareServerAssociation",
                                                   back_populates="server")

    # software_list = db.relationship("SoftwareServerAssociation", back_populates="server")

    def __init__(self,
                 name: str,
                 granules: t.List[str] = None,
                 dns_or_ip: t.Union[str, ipaddress.IPv4Address,
                                    ipaddress.IPv6Address] = None,
                 port: int = None,
                 gates: t.List[t.Union[TGate, t.Dict[str, t.Any]]] = None,
                 me: bool = False,
                 created_on=None,
                 **kwargs):
        super().__init__(**kwargs)
        self.name = name
        if port or dns_or_ip:
            self.add_new_gate(dns_or_ip or self.name, port
                              or defaults.DEFAULT_PORT)

        if gates:
            for gate in gates:
                if isinstance(gate, str):
                    if ':' in gate:
                        self.add_new_gate(*gate.split(':'))
                    else:
                        self.add_new_gate(gate, defaults.DEFAULT_PORT)
                elif isinstance(gate, tuple):
                    self.add_new_gate(*gate)
                elif isinstance(gate, dict):
                    gate['server'] = self
                    Gate.from_json(gate)

        assert 'all' not in (granules or [])
        self.granules = granules or []
        self._me = me
        self.created_on = created_on or get_now()
        # create an empty route
        if not me and not self.deleted:
            Route(self)

    @property
    def external_gates(self):
        e_g = []
        for g in self.gates:
            if not g.ip:
                try:
                    ip = ipaddress.ip_address(
                        socket.getaddrinfo(g.dns,
                                           0,
                                           family=socket.AF_INET,
                                           proto=socket.IPPROTO_TCP)[0][4][0])
                except socket.gaierror:
                    e_g.append(g)
                    continue
                except KeyError:
                    e_g.append(g)
                    continue
            else:
                ip = g.ip
            if not ip.is_loopback:
                e_g.append(g)
        return e_g

    @property
    def localhost_gates(self):
        l_g = []
        for g in self.gates:
            if not g.ip:
                try:
                    ip = ipaddress.ip_address(
                        socket.getaddrinfo(g.dns,
                                           0,
                                           family=socket.AF_INET,
                                           proto=socket.IPPROTO_TCP)[0][4][0])
                except socket.gaierror:
                    continue
                except KeyError:
                    continue
            else:
                ip = g.ip
            if ip.is_loopback:
                l_g.append(g)
        return l_g

    @property
    def hidden_gates(self):
        hg = [g for g in self.gates if g.hidden]
        hg.sort(key=lambda x: x.last_modified_at or get_now())
        return hg

    def add_new_gate(self,
                     dns_or_ip: t.Union[str, ipaddress.IPv4Address,
                                        ipaddress.IPv6Address],
                     port: int,
                     hidden=None):
        ip = None
        dns = None
        if dns_or_ip:
            try:
                ip = ipaddress.ip_address(dns_or_ip)
            except ValueError:
                dns = dns_or_ip
        return Gate(server=self, port=port, dns=dns, ip=ip, hidden=hidden)

    def __str__(self, ):
        return f"{self.name}"

    def url(self, view: str = None, **values) -> str:
        """
        generates the full url to access the server. Uses url_for to generate the full_path.

        Parameters
        ----------
        view
        values

        Raises
        -------
        ConnectionError:
            if server is unreachable
        """
        try:
            scheme = 'http' if current_app.dm and 'keyfile' not in current_app.dm.config.http_conf else 'https'
        except:
            scheme = 'https'
        gate = None
        route = self.route
        if self._me and (route is None or route.cost is None):
            if len(self.localhost_gates) > 0:
                gate = self.localhost_gates[0]
            else:
                # current_app.logger.warning(
                #     f"No localhost set for '{self}'. Trying connection through another gate")
                if len(self.gates) == 0:
                    raise RuntimeError(f"No gate set for server '{self}'")
                gate = self.gates[0]
        elif getattr(route, 'cost', None) == 0:
            gate = route.gate
        elif getattr(route, 'proxy_server', None):
            gate = getattr(getattr(route.proxy_server, 'route', None), 'gate',
                           None)

        if not gate:
            raise errors.UnreachableDestination(self,
                                                getattr(g, 'server', None))

        root_path = f"{scheme}://{gate}"

        if view is None:
            return root_path
        else:
            with current_app.test_request_context():
                return root_path + url_for(view, **values)

    @classmethod
    def get_neighbours(cls, exclude: t.Union[t.Union[Id, 'Server'], t.List[t.Union[Id, 'Server']]] = None,
                       session=None) -> \
            t.List[
                'Server']:
        """returns neighbour servers

        Args:
            alive: if True, returns neighbour servers inside the cluster

        Returns:

        """
        if session:
            query = session.query(cls).filter_by(deleted=0)
        else:
            query = cls.query
        query = query.join(cls.route).filter(Route.cost == 0)
        if exclude:
            if isinstance(exclude, list):
                if isinstance(exclude[0], Server):
                    query = query.filter(
                        Server.id.notin_([s.id for s in exclude]))
                else:
                    query = query.filter(Server.id.notin_(exclude))
            elif isinstance(exclude, Server):
                query = query.filter(Server.id != exclude.id)
            else:
                query = query.filter(Server.id != exclude)

        return query.order_by(cls.name).all()

    @classmethod
    def get_not_neighbours(cls, session=None) -> t.List['Server']:
        if session:
            query = session.query(cls).filter_by(deleted=0)
        else:
            query = cls.query
        return query.outerjoin(cls.route).filter(
            or_(or_(Route.cost > 0, Route.cost == None),
                cls.route == None)).filter(cls._me == False).order_by(
                    cls.name).all()

    @classmethod
    def get_reachable_servers(
        cls,
        alive=False,
        exclude: t.Union[t.Union[Id, 'Server'],
                         t.List[t.Union[Id, 'Server']]] = None
    ) -> t.List['Server']:
        """returns list of reachable servers

        Args:
            alive: if True, returns servers inside the cluster
            exclude: filter to exclude servers

        Returns:
        list of all reachable servers
        """
        query = cls.query.join(cls.route).filter(Route.cost.isnot(None))
        if exclude:
            if is_iterable_not_string(exclude):
                c_exclude = [
                    e.id if isinstance(e, Server) else e for e in exclude
                ]
            else:
                c_exclude = [
                    exclude.id if isinstance(exclude, Server) else exclude
                ]
            query = query.filter(Server.id.notin_(c_exclude))

        if alive:
            query = query.filter(
                Server.id.in_([
                    iden
                    for iden in current_app.dm.cluster_manager.get_alive()
                ]))

        return query.order_by(Server.name).all()

    def to_json(self,
                add_gates=False,
                human=False,
                add_ignore=False,
                **kwargs):
        data = super().to_json(**kwargs)
        data.update({
            'name':
            self.name,
            'granules':
            self.granules,
            'created_on':
            self.created_on.strftime(defaults.DATETIME_FORMAT)
        })
        if add_gates:
            data.update(gates=[])
            for g in self.gates:
                json_gate = g.to_json(human=human)
                json_gate.pop('server_id', None)
                json_gate.pop('server', None)  # added to remove when human set
                data['gates'].append(json_gate)
        if add_ignore:
            data.update(ignore_on_lock=self.l_ignore_on_lock or False)
        return data

    @classmethod
    def from_json(cls, kwargs) -> 'Server':
        kwargs = copy.deepcopy(kwargs)
        gates = kwargs.pop('gates', [])
        if 'created_on' in kwargs:
            kwargs['created_on'] = dt.datetime.strptime(
                kwargs['created_on'], defaults.DATETIME_FORMAT)
        server = super().from_json(kwargs)
        for gate in gates:
            gate.update(server=server)
            Gate.from_json(gate)
        return server

    @classmethod
    def get_current(cls, session=None) -> 'Server':
        if session is None:
            session = db.session
        return session.query(cls).filter_by(_me=True).filter_by(
            deleted=False).one()

    @staticmethod
    def set_initial(session=None, gates=None) -> Id:
        logger = logging.getLogger('dm.db')
        if session is None:
            session = db.session
        server = session.query(Server).filter_by(_me=True).all()
        if len(server) == 0:
            try:
                server_name = current_app.config.get(
                    'SERVER_NAME') or defaults.HOSTNAME
            except:
                server_name = defaults.HOSTNAME

            if gates is None:
                gates = [(ip, defaults.DEFAULT_PORT) for ip in get_ips()]
            server = Server(name=server_name, gates=gates, me=True)
            logger.info(
                f'Creating Server {server.name} with the following gates: {gates}'
            )
            session.add(server)
            return server.id
        elif len(server) > 1:
            raise ValueError('Multiple servers found as me.')
        else:
            return server[0].id

    def ignore_on_lock(self, value: bool):
        if value != self.l_ignore_on_lock:
            from dimensigon.domain.entities import bypass_datamark_update
            with bypass_datamark_update:
                self.l_ignore_on_lock = value

    def set_route(self, proxy_route, gate=None, cost=None):
        if self.route is None:
            self.route = Route(destination=self)
        if isinstance(proxy_route, RouteContainer):
            self.route.set_route(proxy_route)
        elif isinstance(proxy_route, Route):
            assert proxy_route.destination == self
            self.route.set_route(
                RouteContainer(proxy_route.proxy_server, proxy_route.gate,
                               proxy_route.cost))
        else:
            self.route.set_route(RouteContainer(proxy_route, gate, cost))

    def delete(self):
        super().delete()
        for g in self.gates:
            g.delete()
        for l in self.log_sources:
            l.delete()
        for l in self.log_destinations:
            l.delete()
        for ssa in self.software_server_associations:
            ssa.delete()
        for fsa in self.file_server_associations:
            fsa.delete()
        for f in self.files:
            f.delete()
示例#6
0
class File(UUIDistributedEntityMixin, SoftDeleteMixin, db.Model):
    __tablename__ = 'D_file'
    order = 20

    src_server_id = db.Column(typos.UUID, db.ForeignKey('D_server.id'), nullable=False)
    target = db.Column(db.Text, nullable=False)
    dest_folder = db.Column(db.Text)
    _old_target = db.Column("$$target", db.Text)
    l_mtime = db.Column(db.INTEGER)

    source_server = db.relationship("Server")
    destinations: t.List[FileServerAssociation] = db.relationship("FileServerAssociation", lazy='joined')

    __table_args__ = (db.UniqueConstraint('src_server_id', 'target'),)

    query_class = QueryWithSoftDelete

    def __init__(self, source_server: Server, target: str,
                 dest_folder=None, destination_servers: Destination_Servers = None, **kwargs):
        super().__init__(**kwargs)

        self.source_server = source_server
        self.target = target
        self.dest_folder = dest_folder
        dest = []
        for ds in destination_servers or []:
            if isinstance(ds, Server):
                dest.append(FileServerAssociation(file=self, destination_server=ds))
            elif isinstance(ds, dict):

                dest.append(
                    FileServerAssociation(file=self, destination_server=Server.query.get(ds.get('dst_server_id')),
                                          dest_folder=ds.get('dest_folder')))
            elif isinstance(ds, tuple):
                if isinstance(ds[0], Server):
                    dest.append(FileServerAssociation(file=self, destination_server=ds[0], dest_folder=ds[1]))
                else:
                    dest.append(
                        FileServerAssociation(file=self, destination_server=Server.query.get(ds[0]), dest_folder=ds[1]))
        self.destinations = dest

    def __str__(self):
        return f"{self.source_server}:{self.target}"

    def to_json(self, human=False, destinations=False, include: t.List[str] = None,
                exclude: t.List[str] = None, **kwargs):
        data = super().to_json(**kwargs)
        if self.source_server.id is None:
            raise RuntimeError('Set ids for servers before')
        data.update(target=self.target, dest_folder=self.dest_folder)
        if human:
            data.update(src_server=str(self.source_server.name))
            if destinations:
                dest = []
                for d in self.destinations:
                    dest.append(dict(dst_server=d.destination_server.name, dest_folder=d.dest_folder))
                data.update(destinations=dest)
        else:
            data.update(src_server_id=str(self.source_server.id))
            if destinations:
                dest = []
                for d in self.destinations:
                    dest.append(dict(dst_server_id=d.destination_server.id, dest_folder=d.dest_folder))
                data.update(destinations=dest)

        if include:
            data = {k: v for k, v in data.items() if k in include}
        if exclude:
            data = {k: v for k, v in data.items() if k not in exclude}
        return data

    @classmethod
    def from_json(cls, kwargs) -> 'File':
        kwargs = copy.deepcopy(kwargs)
        kwargs['source_server'] = Server.query.get_or_raise(kwargs.pop('src_server_id'))
        return super().from_json(kwargs)

    def delete(self):
        for d in self.destinations:
            d.delete()
        return super().delete()
示例#7
0
class Step(UUIDistributedEntityMixin, db.Model):
    __tablename__ = "D_step"
    order = 30
    orchestration_id = db.Column(UUID, db.ForeignKey('D_orchestration.id'), nullable=False)
    action_template_id = db.Column(UUID, db.ForeignKey('D_action_template.id'))
    undo = db.Column(db.Boolean, nullable=False)
    step_stop_on_error = db.Column("stop_on_error", db.Boolean)
    step_stop_undo_on_error = db.Column("stop_undo_on_error", db.Boolean)
    step_undo_on_error = db.Column("undo_on_error", db.Boolean)
    step_expected_stdout = db.Column("expected_stdout", db.Text)
    step_expected_stderr = db.Column("expected_stderr", db.Text)
    step_expected_rc = db.Column("expected_rc", db.Integer)
    step_system_kwargs = db.Column("system_kwargs", db.JSON)
    target = db.Column(ScalarListType(str))
    created_on = db.Column(UtcDateTime(), nullable=False, default=get_now)
    step_action_type = db.Column("action_type", typos.Enum(ActionType))
    step_code = db.Column("code", db.Text)
    step_post_process = db.Column("post_process", db.Text)
    step_pre_process = db.Column("pre_process", db.Text)
    step_name = db.Column("name", db.String(40))
    step_schema = db.Column("schema", db.JSON)
    step_description = db.Column("description", db.Text)

    orchestration = db.relationship("Orchestration", primaryjoin="Step.orchestration_id==Orchestration.id",
                                    back_populates="steps")
    action_template = db.relationship("ActionTemplate", primaryjoin="Step.action_template_id==ActionTemplate.id",
                                      backref="steps")

    parent_steps = db.relationship("Step", secondary="D_step_step",
                                   primaryjoin="D_step.c.id==D_step_step.c.step_id",
                                   secondaryjoin="D_step.c.id==D_step_step.c.parent_step_id",
                                   back_populates="children_steps")

    children_steps = db.relationship("Step", secondary="D_step_step",
                                     primaryjoin="D_step.c.id==D_step_step.c.parent_step_id",
                                     secondaryjoin="D_step.c.id==D_step_step.c.step_id",
                                     back_populates="parent_steps")

    def __init__(self, orchestration: 'Orchestration', undo: bool, action_template: 'ActionTemplate' = None,
                 action_type: ActionType = None, code: str = None, pre_process: str = None, post_process: str = None,
                 stop_on_error: bool = None, stop_undo_on_error: bool = None, undo_on_error: bool = None,
                 expected_stdout: t.Optional[str] = None,
                 expected_stderr: t.Optional[str] = None,
                 expected_rc: t.Optional[int] = None,
                 schema: t.Dict[str, t.Any] = None,
                 system_kwargs: t.Dict[str, t.Any] = None,
                 parent_steps: t.List['Step'] = None, children_steps: t.List['Step'] = None,
                 target: t.Union[str, t.Iterable[str]] = None, name: str = None, description: str = None, rid=None,
                 **kwargs):

        super().__init__(**kwargs)
        assert undo in (False, True)
        if action_template is not None:
            assert action_type is None
        else:
            assert action_type is not None
        self.undo = undo
        self.step_stop_on_error = stop_on_error if stop_on_error is not None else kwargs.pop('step_stop_on_error', None)
        self.step_stop_undo_on_error = stop_undo_on_error if stop_undo_on_error is not None else kwargs.pop(
            'step_stop_undo_on_error', None)
        self.step_undo_on_error = undo_on_error if undo_on_error is not None else kwargs.pop('step_undo_on_error', None)
        self.action_template = action_template
        self.step_action_type = action_type if action_type is not None else kwargs.pop('step_action_type', None)

        expected_stdout = expected_stdout if expected_stdout is not None else kwargs.pop(
            'step_expected_stdout', None)
        self.step_expected_stdout = '\n'.join(expected_stdout) if is_iterable_not_string(
            expected_stdout) else expected_stdout

        expected_stderr = expected_stderr if expected_stderr is not None else kwargs.pop(
            'step_expected_stderr', None)
        self.step_expected_stderr = '\n'.join(expected_stderr) if is_iterable_not_string(
            expected_stderr) else expected_stderr

        self.step_expected_rc = expected_rc if expected_rc is not None else kwargs.pop('step_expected_rc', None)
        self.step_schema = schema if schema is not None else kwargs.pop('step_schema', None) or {}
        self.step_system_kwargs = system_kwargs if system_kwargs is not None else kwargs.pop('step_system_kwargs',
                                                                                             None) or {}
        code = code if code is not None else kwargs.pop('step_code', None)
        self.step_code = '\n'.join(code) if is_iterable_not_string(code) else code

        post_process = post_process if post_process is not None else kwargs.pop('step_post_process', None)
        self.step_post_process = '\n'.join(post_process) if is_iterable_not_string(post_process) else post_process

        pre_process = pre_process if pre_process is not None else kwargs.pop('step_pre_process', None)
        self.step_pre_process = '\n'.join(pre_process) if is_iterable_not_string(pre_process) else pre_process

        self.orchestration = orchestration
        self.parent_steps = parent_steps or []
        self.children_steps = children_steps or []
        if self.undo is False:
            if target is None:
                self.target = ['all']
            else:
                self.target = [target] if isinstance(target, str) else (target if len(target) > 0 else ['all'])
        else:
            if target:
                raise errors.BaseError('target must not be set when creating an UNDO step')
        self.created_on = kwargs.get('created_on') or get_now()
        self.step_name = name if name is not None else kwargs.pop('step_name', None)

        description = description if description is not None else kwargs.pop('step_description', None)
        self.step_description = '\n'.join(description) if is_iterable_not_string(description) else description
        self.rid = rid  # used when creating an Orchestration

    @orm.reconstructor
    def init_on_load(self):
        self.system_kwargs = self.system_kwargs or {}

    @property
    def parents(self):
        return self.parent_steps

    @property
    def children(self):
        return self.children_steps

    @property
    def parent_undo_steps(self):
        return [s for s in self.parent_steps if s.undo == True]

    @property
    def children_undo_steps(self):
        return [s for s in self.children_steps if s.undo == True]

    @property
    def parent_do_steps(self):
        return [s for s in self.parent_steps if s.undo == False]

    @property
    def children_do_steps(self):
        return [s for s in self.children_steps if s.undo == False]

    @property
    def stop_on_error(self):
        return self.step_stop_on_error if self.step_stop_on_error is not None else self.orchestration.stop_on_error

    @stop_on_error.setter
    def stop_on_error(self, value):
        self.step_stop_on_error = value

    @property
    def stop_undo_on_error(self):
        return self.step_stop_undo_on_error if self.step_stop_undo_on_error is not None \
            else self.orchestration.stop_undo_on_error

    @stop_undo_on_error.setter
    def stop_undo_on_error(self, value):
        self.step_stop_undo_on_error = value

    @property
    def undo_on_error(self):
        if self.undo:
            return None
        return self.step_undo_on_error if self.step_undo_on_error is not None else self.orchestration.undo_on_error

    @undo_on_error.setter
    def undo_on_error(self, value):
        self.step_undo_on_error = value

    @property
    def schema(self):
        if self.action_template:
            schema = dict(ChainMap(self.step_schema, self.action_template.schema))
        else:
            schema = dict(self.step_schema)

        if self.action_type == ActionType.ORCHESTRATION:
            from .orchestration import Orchestration
            mapping = schema.get('mapping', {})
            o = Orchestration.get(mapping.get('orchestration', None), mapping.get('version', None))
            if isinstance(o, Orchestration):
                orch_schema = o.schema
                i = schema.get('input', {})
                i.update(orch_schema.get('input', {}))
                schema.update({'input': i})
                m = schema.get('mapping', {})
                m.update(orch_schema.get('mapping', {}))
                schema.update({'mapping': m})
                r = schema.get('required', [])
                [r.append(k) for k in orch_schema.get('required', []) if k not in r]
                schema.update({'required': r})
                o = schema.get('output', [])
                [o.append(k) for k in orch_schema.get('output', []) if k not in o]
                schema.update({'output': o})
        return schema

    @schema.setter
    def schema(self, value):
        self.step_schema = value

    @property
    def system_kwargs(self):
        if self.action_template:
            return dict(ChainMap(self.step_system_kwargs, self.action_template.system_kwargs))
        else:
            return dict(self.step_system_kwargs)

    @system_kwargs.setter
    def system_kwargs(self, value):
        self.step_system_kwargs = value

    @property
    def code(self):
        if self.step_code is None and self.action_template:
            return self.action_template.code
        else:
            return self.step_code

    @code.setter
    def code(self, value):
        self.step_code = value

    @property
    def action_type(self):
        if self.step_action_type is None and self.action_template:
            return self.action_template.action_type
        else:
            return self.step_action_type

    @action_type.setter
    def action_type(self, value):
        self.step_action_type = value

    @property
    def post_process(self):
        if self.step_post_process is None and self.action_template:
            return self.action_template.post_process
        else:
            return self.step_post_process

    @post_process.setter
    def post_process(self, value):
        self.step_post_process = value

    @property
    def pre_process(self):
        if self.step_pre_process is None and self.action_template:
            return self.action_template.pre_process
        else:
            return self.step_pre_process

    @pre_process.setter
    def pre_process(self, value):
        self.step_pre_process = value

    @property
    def expected_stdout(self):
        if self.step_expected_stdout is None and self.action_template:
            return self.action_template.expected_stdout
        else:
            return self.step_expected_stdout

    @expected_stdout.setter
    def expected_stdout(self, value):
        self.step_expected_stdout = value

    @property
    def expected_stderr(self):
        if self.step_expected_stderr is None and self.action_template:
            return self.action_template.expected_stderr
        else:
            return self.step_expected_stderr

    @expected_stderr.setter
    def expected_stderr(self, value):
        if value == self.action_template.expected_stderr:
            self.step_expected_stderr = None
        else:
            self.step_expected_stderr = value

    @property
    def expected_rc(self):
        if self.step_expected_rc is None and self.action_template:
            return self.action_template.expected_rc
        else:
            return self.step_expected_rc

    @expected_rc.setter
    def expected_rc(self, value):
        if self.action_template and value == self.action_template.expected_rc:
            self.step_expected_rc = None
        else:
            self.step_expected_rc = value

    @property
    def name(self):
        if self.step_name is None and self.action_template:
            return str(self.action_template)
        else:
            return self.step_name

    @name.setter
    def name(self, value):
        self.step_name = value

    @property
    def description(self):
        if self.step_description is None and self.action_template:
            return str(self.action_template)
        else:
            return self.step_description

    @description.setter
    def description(self, value):
        self.step_description = value

    def eq_imp(self, other):
        """
        two steps are equal if they execute the same code with the same parameters even if they are from different
        orchestrations or they are in the same orchestration with different positions

        Parameters
        ----------
        other: Step

        Returns
        -------
        result: bool

        Notes
        -----
        _id and _orchestration are not compared
        """
        if isinstance(other, self.__class__):
            return all([self.undo == other.undo,
                        self.stop_on_error == other.stop_on_error,
                        self.stop_undo_on_error == other.stop_undo_on_error,
                        self.undo_on_error == other.undo_on_error,
                        self.expected_stdout == other.expected_stdout,
                        self.expected_stderr == other.expected_stderr,
                        self.expected_rc == other.expected_rc,
                        self.system_kwargs == other.system_kwargs,
                        self.code == other.code,
                        self.post_process == other.post_process,
                        self.pre_process == other.pre_process,
                        self.action_type == other.action_type,
                        ])
        else:
            raise NotImplemented

    def __str__(self):
        return self.name or self.id

    def __repr__(self):
        return ('Undo ' if self.undo else '') + self.__class__.__name__ + ' ' + str(getattr(self, 'id', ''))

    def _add_parents(self, parents):
        for step in parents:
            if not step in self.parent_steps:
                self.parent_steps.append(step)

    def _remove_parents(self, parents):
        for step in parents:
            if step in self.parent_steps:
                self.parent_steps.remove(step)

    def _add_children(self, children):
        for step in children:
            if not step in self.children_steps:
                self.children_steps.append(step)

    def _remove_children(self, children):
        for step in children:
            if step in self.children_steps:
                self.children_steps.remove(step)

    def to_json(self, add_action=False, split_lines=False, **kwargs):
        data = super().to_json(**kwargs)
        if getattr(self.orchestration, 'id', None):
            data.update(orchestration_id=str(self.orchestration.id))
        if getattr(self.action_template, 'id', None):
            if add_action:
                data.update(action_template=self.action_template.to_json(split_lines=split_lines))
            else:
                data.update(action_template_id=str(self.action_template.id))
        data.update(undo=self.undo)
        data.update(stop_on_error=self.step_stop_on_error) if self.step_stop_on_error is not None else None
        data.update(
            stop_undo_on_error=self.step_stop_undo_on_error) if self.step_stop_undo_on_error is not None else None
        data.update(undo_on_error=self.step_undo_on_error) if self.step_undo_on_error is not None else None
        if self.step_schema:
            data.update(schema=self.step_schema)
        if self.step_expected_stdout is not None:
            data.update(
                expected_stdout=self.step_expected_stdout.split('\n') if split_lines else self.step_expected_stdout)
        if self.step_expected_stderr is not None:
            data.update(
                expected_stderr=self.step_expected_stderr.split('\n') if split_lines else self.step_expected_stderr)
        data.update(expected_rc=self.step_expected_rc) if self.step_expected_rc is not None else None
        data.update(system_kwargs=self.step_system_kwargs) if self.step_system_kwargs is not None else None
        data.update(parent_step_ids=[str(step.id) for step in self.parents])
        if self.step_code is not None:
            data.update(code=self.step_code.split('\n') if split_lines else self.step_code)
        data.update(action_type=self.step_action_type.name) if self.step_action_type is not None else None
        if self.step_post_process is not None:
            data.update(post_process=self.step_post_process.split('\n') if split_lines else self.step_post_process)
        if self.step_pre_process is not None:
            data.update(pre_process=self.step_pre_process.split('\n') if split_lines else self.step_pre_process)
        data.update(created_on=self.created_on.strftime(defaults.DATETIME_FORMAT))
        if self.step_description is not None:
            data.update(description=self.step_description.split('\n') if split_lines else self.step_description)
        if self.step_name is not None:
            data.update(name=self.step_name)

        return data

    @classmethod
    def from_json(cls, kwargs):
        from dimensigon.domain.entities import ActionTemplate, Orchestration
        kwargs = dict(kwargs)
        if 'orchestration_id' in kwargs:
            ident = kwargs.pop('orchestration_id')
            kwargs['orchestration'] = db.session.query(Orchestration).get(ident)
            if kwargs['orchestration'] is None:
                raise errors.EntityNotFound('Orchestration', ident=ident)
        if 'action_template_id' in kwargs:
            ident = kwargs.pop('action_template_id')
            kwargs['action_template'] = db.session.query(ActionTemplate).get(ident)
            if kwargs['action_template'] is None:
                raise errors.EntityNotFound('ActionTemplate', ident=ident)
        if 'action_type' in kwargs:
            kwargs['action_type'] = ActionType[kwargs.pop('action_type')]
        if 'created_on' in kwargs:
            kwargs['created_on'] = datetime.datetime.strptime(kwargs['created_on'], defaults.DATETIME_FORMAT)
        kwargs['parent_steps'] = []
        for parent_step_id in kwargs.pop('parent_step_ids', []):
            ps = Step.query.get(parent_step_id)
            if ps:
                kwargs['parent_steps'].append(ps)
            else:
                raise errors.EntityNotFound('Step', parent_step_id)
        return super().from_json(kwargs)
示例#8
0
class Route(db.Model):
    __tablename__ = 'L_route'

    destination_id = db.Column(UUID,
                               db.ForeignKey('D_server.id'),
                               primary_key=True,
                               nullable=False)
    proxy_server_id = db.Column(UUID, db.ForeignKey('D_server.id'))
    gate_id = db.Column(UUID, db.ForeignKey('D_gate.id'))
    cost = db.Column(db.Integer)

    destination = db.relationship("Server",
                                  foreign_keys=[destination_id],
                                  back_populates="route",
                                  lazy='joined')
    proxy_server = db.relationship("Server",
                                   foreign_keys=[proxy_server_id],
                                   lazy='joined')
    gate = db.relationship("Gate", foreign_keys=[gate_id], lazy='joined')

    def __init__(self,
                 destination: 'Server',
                 proxy_server_or_gate: t.Union['Server', 'Gate'] = None,
                 cost: int = None):
        # avoid cycle import
        from dimensigon.domain.entities import Server
        self.destination = destination
        if isinstance(proxy_server_or_gate, Server):
            proxy_server = proxy_server_or_gate
            gate = None
        else:
            proxy_server = None
            gate = proxy_server_or_gate
        if proxy_server:
            if proxy_server == destination:
                raise ValueError(
                    'You must specify a gate when proxy_server equals destination'
                )
            else:
                if cost is None or cost == 0:
                    raise ValueError(
                        "Cost must be specified and greater than 0 when proxy_server"
                    )
                self.proxy_server = proxy_server
                self.cost = cost
        elif gate:
            # check if gate is from neighbour or from a proxy server
            if destination == gate.server:
                if cost is not None and cost > 0:
                    raise ValueError(
                        "Cost must be set to 0 when defining route for a neighbour"
                    )
                self.gate = gate
                self.cost = 0
            else:
                if cost is None or cost <= 0:
                    raise ValueError(
                        "Cost must be specified and greater than 0 when gate is from a proxy_server"
                    )
                else:
                    self.proxy_server = gate.server
                    self.cost = cost
        elif cost == 0:
            # find a gateway and set that gateway as default
            if len(destination.external_gates) == 1:
                self.gate = destination.external_gates[0]
                self.cost = 0
            else:
                for gate in destination.external_gates:
                    if check_host(gate.dns or str(gate.ip),
                                  gate.port,
                                  timeout=1,
                                  retry=3,
                                  delay=0.5):
                        self.gate = gate
                        self.cost = 0
                        break
        # if not (self.gate or self.proxy_server):
        #     raise ValueError('Not a valid route')

    def validate_route(self, rc: RouteContainer):
        if rc.proxy_server:
            if not (rc.gate is None and rc.cost > 0):
                raise errors.InvalidRoute(self.destination, rc)
            if rc.proxy_server._me:
                raise errors.InvalidRoute(self.destination, rc)
        elif rc.gate:
            if not rc.cost == 0:
                raise errors.InvalidRoute(self.destination, rc)
        else:
            if rc.cost is not None:
                raise errors.InvalidRoute(self.destination, rc)

    def set_route(self, rc: RouteContainer):
        self.validate_route(rc)
        self.proxy_server, self.gate, self.cost = rc

    def __str__(self):
        return f"{self.destination} -> " \
               f"{self.proxy_server or self.gate}, {self.cost}"

    def __repr__(self):
        return f"Route({self.to_json()})"

    def to_json(self, human=False):
        if not self.destination.id or (self.proxy_server
                                       and not self.proxy_server.id) or (
                                           self.gate and not self.gate.id):
            raise RuntimeError("commit object before dump to json")
        if human:
            return {
                'destination':
                str(self.destination) if self.destination else None,
                'proxy_server':
                str(self.proxy_server) if self.proxy_server else None,
                'gate': str(self.gate) if self.gate else None,
                'cost': self.cost
            }
        else:
            return {
                'destination_id': self.destination_id,
                'proxy_server_id': self.proxy_server_id,
                'gate_id': self.gate_id,
                'cost': self.cost
            }
示例#9
0
class Log(UUIDistributedEntityMixin, SoftDeleteMixin, db.Model):
    __tablename__ = 'D_log'

    src_server_id = db.Column(typos.UUID,
                              db.ForeignKey('D_server.id'),
                              nullable=False)
    target = db.Column(db.Text, nullable=False)
    include = db.Column(db.Text)
    exclude = db.Column(db.Text)
    dst_server_id = db.Column(typos.UUID,
                              db.ForeignKey('D_server.id'),
                              nullable=False)
    mode = db.Column(typos.Enum(Mode))
    dest_folder = db.Column(db.Text)
    recursive = db.Column(db.Boolean, default=False)
    _old_target = db.Column("$$target", db.Text)

    source_server = db.relationship("Server",
                                    foreign_keys=[src_server_id],
                                    back_populates="log_sources")
    destination_server = db.relationship("Server",
                                         foreign_keys=[dst_server_id],
                                         back_populates="log_destinations")

    __table_args__ = (db.UniqueConstraint('src_server_id', 'target',
                                          'dst_server_id'), )

    def __init__(self,
                 source_server: 'Server',
                 target: str,
                 destination_server: 'Server',
                 dest_folder=None,
                 include=None,
                 exclude=None,
                 recursive=False,
                 mode=Mode.REPO_MIRROR,
                 **kwargs):
        super().__init__(**kwargs)

        self.source_server = source_server
        self.target = target
        self.destination_server = destination_server
        self.dest_folder = dest_folder
        if self.dest_folder is None:
            self.mode = mode
        else:
            self.mode = Mode.FOLDER
        self.include = include
        self._re_include = re.compile(self.include or '')
        self.exclude = exclude
        self._re_exclude = re.compile(self.exclude or '^$')
        self.recursive = recursive

    def __str__(self):
        return f"{self.source_server}:{self.target} -> {self.destination_server}:{self.dest_folder}"

    def to_json(self,
                human=False,
                include: t.List[str] = None,
                exclude: t.List[str] = None,
                **kwargs):
        data = super().to_json(**kwargs)
        if self.source_server.id is None or self.destination_server.id is None:
            raise RuntimeError('Set ids for servers before')
        data.update(target=self.target,
                    include=self.include,
                    exclude=self.exclude,
                    dest_folder=self.dest_folder,
                    recursive=self.recursive,
                    mode=self.mode.name)
        if human:
            data.update(src_server=str(self.source_server.name),
                        dst_server=str(self.destination_server.name))
        else:
            data.update(src_server_id=str(self.source_server.id),
                        dst_server_id=str(self.destination_server.id))

        if include:
            data = {k: v for k, v in data.items() if k in include}
        if exclude:
            data = {k: v for k, v in data.items() if k not in exclude}
        return data

    @classmethod
    def from_json(cls, kwargs) -> 'Log':
        from dimensigon.domain.entities import Server
        kwargs = copy.deepcopy(kwargs)
        kwargs['mode'] = Mode[kwargs.get('mode')]
        kwargs['source_server'] = Server.query.get(kwargs.pop('src_server_id'))
        kwargs['destination_server'] = Server.query.get(
            kwargs.pop('dst_server_id'))
        return super().from_json(kwargs)
示例#10
0
class Orchestration(UUIDistributedEntityMixin, db.Model):
    __tablename__ = 'D_orchestration'
    order = 20

    name = db.Column(db.String(80), nullable=False)
    version = db.Column(db.Integer, nullable=False)
    description = db.Column(db.Text)
    stop_on_error = db.Column(db.Boolean)
    stop_undo_on_error = db.Column(db.Boolean)
    undo_on_error = db.Column(db.Boolean)
    created_at = db.Column(UtcDateTime(timezone=True), default=get_now)

    steps = db.relationship(
        "Step",
        primaryjoin="Step.orchestration_id==Orchestration.id",
        back_populates="orchestration")

    __table_args__ = (db.UniqueConstraint('name',
                                          'version',
                                          name='D_orchestration_uq01'), )

    def __init__(self,
                 name: str,
                 version: int,
                 description: t.Optional[str] = None,
                 steps: t.List[Step] = None,
                 stop_on_error: bool = True,
                 stop_undo_on_error: bool = True,
                 undo_on_error: bool = True,
                 dependencies: Tdependencies = None,
                 created_at=None,
                 **kwargs):
        super().__init__(**kwargs)

        self.name = name
        self.version = version
        self.description = '\n'.join(description) if is_iterable_not_string(
            description) else description
        self.steps = steps or []
        assert isinstance(stop_on_error, bool)
        self.stop_on_error = stop_on_error
        assert isinstance(stop_undo_on_error, bool)
        self.stop_undo_on_error = stop_undo_on_error
        assert isinstance(undo_on_error, bool)
        self.undo_on_error = undo_on_error
        self.created_at = created_at or get_now()

        if dependencies:
            self.set_dependencies(dependencies)
        else:
            self._graph = DAG()

    @orm.reconstructor
    def init_on_load(self):
        self._graph = DAG()
        for step in self.steps:
            if step.parents:
                for p in step.parents:
                    self._graph.add_edge(p, step)
            else:
                self._graph.add_node(step)

    def set_dependencies(self, dependencies: Tdependencies):
        edges = []
        find = lambda id_: next(
            (step for step in self.steps if step.id == id_))
        if isinstance(dependencies, t.Dict):
            for k, v in dependencies.items():
                try:
                    step_from = find(k)
                except StopIteration:
                    raise ValueError(f'id step {k} not found in steps list')
                for child_id in v:
                    try:
                        step_to = find(child_id)
                    except StopIteration:
                        raise ValueError(
                            f'id step {child_id} not found in steps list')
                    edges.append((step_from, step_to))
        elif isinstance(dependencies, t.Iterable):
            edges = dependencies
        else:
            raise ValueError(
                f'dependencies must be a dict like object or an iterable of tuples. '
                f'See the docs for more information')
        self._graph = DAG(edges)

    @property
    def parents(self) -> t.Dict[Step, t.List[Step]]:
        return self._graph.pred

    @property
    def children(self) -> t.Dict[Step, t.List[Step]]:
        return self._graph.succ

    @property
    def dependencies(self) -> t.Dict[Step, t.List[Step]]:
        return self.children

    @property
    def root(self) -> t.List[Step]:
        return self._graph.root

    @property
    def target(self) -> t.Set[str]:
        target = set()
        for step in self.steps:
            if step.undo is False:
                target.update(step.target)
        return target

    def _step_exists(self, step: t.Union[t.List[Step], Step]):
        """Checks if all the steps belong to the current orchestration

        Parameters
        ----------
        step: list[Step]|Step
            Step or list of steps to be evaluated

        Returns
        -------
        None

        Raises
        ------
        ValueError: if any step passed as argument is not in the orchestration
        """
        if not isinstance(step, list):
            steps = [step]
        else:
            steps = step

        for step in steps:
            if not (step in self._graph.nodes and step.orchestration is self):
                raise ValueError(f'{step} is NOT from this orchestration')

    def _check_dependencies(self, step, parents=None, children=None):
        """
        Checks if the dependencies that are going to be added accomplish the business rules. These rules are:
            1. a 'do' Step cannot be preceded for an 'undo' Step
            2. cannot be cycles inside the orchestration

        Parameters
        ----------
        step: Step
            step to be evaluated
        parents: list[Step]
            parent steps to be added
        children: list[Step]
            children steps to be added

        Raises
        ------
        ValueError
            if the rule 1 is not passed
        CycleError
            if the rule 2 is not passed
        """
        parents = parents or []
        children = children or []
        if parents:
            if any([p.undo for p in parents]) and not step.undo:
                attr = 'rid' if step.rid is not None else 'id'
                raise errors.ParentUndoError(getattr(
                    step, attr), [getattr(s, attr) for s in parents if s.undo])
        if children:
            if any([not c.undo for c in children]) and step.undo:
                attr = 'rid' if step.rid is not None else 'id'
                raise errors.ChildDoError(
                    getattr(step, attr),
                    [getattr(s, attr) for s in children if s.undo])
        g = self._graph.copy()
        g.add_edges_from([(p, step) for p in parents])
        g.add_edges_from([(step, c) for c in children])
        if g.is_cyclic():
            raise errors.CycleError()

    def add_step(self, *args, parents=None, children=None, **kwargs) -> Step:
        """Allows to add step into the orchestration

        :param args: args passed to Step: undo, action_template.
        :param parents: list of parent steps.
        :param children: list of children steps.
        :param kwargs: keyword arguments passed to the Step
        :return: The step created.
        """
        parents = parents or []
        children = children or []
        s = Step(None, *args, **kwargs)
        self._step_exists(parents + children)
        self._check_dependencies(s, parents, children)
        s.orchestration = self
        self._graph.add_node(s)
        self.add_parents(s, parents)
        self.add_children(s, children)
        return s

    def delete_step(self, step: Step) -> 'Orchestration':
        """
        Allows to remove a Step from the orchestration

        Parameters
        ----------
        step Step: step to remove from the orchestration
        """
        self._step_exists(step)
        i = self.steps.index(step)
        self.steps.pop(i)
        self._graph.remove_node(step)
        return self

    def add_parents(self, step: Step,
                    parents: t.List[Step]) -> 'Orchestration':
        """add_parents adds the parents passed into the step. No remove from previous parents

        Parameters
        ----------
        step: Step
            step to add parents
        parents: list
            list of parent steps to add

        See Also
        --------
        set_parents, delete_parents
        add_children, set_children, delete_children

        Examples
        --------
        >>> at = ActionTemplate(name='action', version=1, action_type=ActionType.SHELL, code='code to run',
                                expected_output='',
                                expected_rc=0, system_kwargs={})
        >>> o = Orchestration('Test Orchestration', 1, DAG(), 'description')
        >>> s1 = o.add_step(undo=False, action_template=at, parents=[], children=[], stop_on_error=False)
        >>> s2 = o.add_step(undo=False, action_template=at, parents=[], children=[], stop_on_error=False)
        >>> s3 = o.add_step(undo=False, action_template=at, parents=[], children=[], stop_on_error=False)
        >>> o.add_parents(s2, [s1])
        >>> o.parents[s2]
        [Step1]
        >>> o.add_parents(s2, [s3])
        >>> o.parents[s2]
        [Step1, Step3]
        """
        self._step_exists([step] + list(parents))
        self._check_dependencies(step, parents=parents)
        step._add_parents(parents)
        self._graph.add_edges_from([(p, step) for p in parents])
        return self

    def delete_parents(self, step: Step,
                       parents: t.List[Step]) -> 'Orchestration':
        """delete_parents deletes the parents passed from the step.

        Parameters
        ----------
        step: Step
            step to remove parents
        parents: list
            list of parent steps to remove

        See Also
        --------
        add_parents, set_parents
        add_children, set_children, delete_children

        Examples
        --------
        >>> at = ActionTemplate(name='action', version=1, action_type=ActionType.SHELL, code='code to run',
                                expected_output='',
                                expected_rc=0, system_kwargs={})
        >>> o = Orchestration('Test Orchestration', 1, DAG(), 'description')
        >>> s1 = o.add_step(undo=False, action_template=at, parents=[], children=[], stop_on_error=False)
        >>> s2 = o.add_step(undo=False, action_template=at, parents=[], children=[], stop_on_error=False)
        >>> s3 = o.add_step(undo=False, action_template=at, parents=[], children=[], stop_on_error=False)
        >>> o.add_children(s1, [s2, s3])
        >>> o.children[s1]
        [Step2, Step3]
        >>> o.delete_parents(s3, [s1])
        >>> o.children[s1]
        [Step2]
        """
        self._step_exists([step] + list(parents))
        step._remove_parents(parents)
        self._graph.remove_edges_from([(p, step) for p in parents])
        return self

    def set_parents(self, step: Step,
                    parents: t.List[Step]) -> 'Orchestration':
        """set_parents sets the parents passed on the step, removing the previos ones

        Parameters
        ----------
        step: Step
            step to remove parents
        parents: list
            list of parent steps to set

        See Also
        --------
        add_parents, delete_parents
        add_children, delete_children, set_children,

        Examples
        --------
        >>> at = ActionTemplate(name='action', version=1, action_type=ActionType.SHELL, code='code to run',
                                expected_rc=0, system_kwargs={})
        >>> o = Orchestration('Test Orchestration', 1, DAG(), 'description')
        >>> s1 = o.add_step(undo=False, action_template=at, parents=[], children=[], stop_on_error=False)
        >>> s2 = o.add_step(undo=False, action_template=at, parents=[], children=[], stop_on_error=False)
        >>> s3 = o.add_step(undo=False, action_template=at, parents=[], children=[], stop_on_error=False)
        >>> o.add_parents(s1, [s2])
        >>> o.parents[s1]
        [Step2]
        >>> o.set_parents(s1, [s3])
        >>> o.parents[s1]
        [Step3]
        """
        self.delete_parents(step, self._graph.pred[step])
        self.add_parents(step, parents)
        return self

    def add_children(self, step: Step,
                     children: t.List[Step]) -> 'Orchestration':
        self._step_exists([step] + children)
        self._check_dependencies(step, children=children)
        step._add_children(children)
        self._graph.add_edges_from([(step, c) for c in children])
        return self

    def delete_children(self, step: Step,
                        children: t.List[Step]) -> 'Orchestration':
        self._step_exists([step] + children)
        step._remove_children(children)
        self._graph.remove_edges_from([(step, c) for c in children])
        return self

    def set_children(self, step: Step,
                     children: t.List[Step]) -> 'Orchestration':
        self.delete_children(step, self._graph.succ[step])
        self.add_children(step, children)
        return self

    def eq_imp(self, other: 'Orchestration') -> bool:
        """
        compares if two orchestrations implement same steps with same parameters and dependencies

        Parameters
        ----------
        other: Orchestration

        Returns
        -------
        result: bool
        """
        if isinstance(other, self.__class__):
            if len(self.steps) != len(other.steps):
                return False
            res = []
            for s in self.steps:
                res.append(any(map(lambda x: s.eq_imp(x), other.steps)))

            if all(res):

                matched_steps = []
                res2 = []
                v2 = []
                for k1, v1 in self.children.items():
                    k2 = None
                    for s in filter(lambda x: x not in matched_steps,
                                    other.steps):
                        if k1.eq_imp(s):
                            k2 = s
                            v2 = other.children[k2]
                            break
                    if not k2:
                        raise RuntimeError('Step not found in other')
                    matched_steps.append(k2)
                    if len(v1) != len(v2):
                        return False

                    for s in v1:
                        res2.append(any(map(lambda x: s.eq_imp(x), v2)))
                return all(res2)
            else:
                return False
        else:
            return False

    def subtree(
        self, steps: t.Union[t.List[Step], t.Iterable[Step]]
    ) -> t.Dict[Step, t.List[Step]]:
        return self._graph.subtree(steps)

    def to_json(self,
                add_target=False,
                add_params=False,
                add_steps=False,
                add_action=False,
                split_lines=False,
                add_schema=False,
                **kwargs):
        data = super().to_json(**kwargs)
        data.update(name=self.name,
                    version=self.version,
                    stop_on_error=self.stop_on_error,
                    undo_on_error=self.undo_on_error,
                    stop_undo_on_error=self.stop_undo_on_error)
        if add_target:
            data.update(target=list(self.target))
        if add_steps:
            json_steps = []
            for step in self.steps:
                json_step = step.to_json(add_action=add_action,
                                         split_lines=split_lines)
                json_step.pop('orchestration_id')
                json_steps.append(json_step)
            data['steps'] = json_steps
        if add_schema:
            data['schema'] = self.schema
        return data

    @classmethod
    def from_json(cls, kwargs):
        return super().from_json(kwargs)

    def __str__(self):
        return f"{self.name}.{self.version}"

    @property
    def schema(self):
        from ...use_cases.deployment import reserved_words, extract_container_var
        outer_schema = {'required': set(), 'output': set()}
        level = 1
        while level <= self._graph.depth:
            new_schema = copy.deepcopy(outer_schema)
            for step in self._graph.get_nodes_at_level(level):
                schema = step.schema
                container_names = [
                    k for k in schema.keys() if k not in reserved_words
                ]
                for cn in container_names:
                    for k, v in schema.get(cn, {}).items():
                        if cn not in new_schema:
                            new_schema.update({cn: {}})
                        if not (k in outer_schema['output']
                                or k in schema.get('mapping', {})):
                            new_schema[cn].update({k: v})

                for k in schema.get('required', []):
                    cn, v = extract_container_var(k)
                    nv = f"{cn}.{v}"
                    if cn == 'input':
                        if v not in outer_schema[
                                'output'] and v not in schema.get(
                                    'mapping', {}):
                            new_schema['required'].add(nv)
                    elif cn != 'env':
                        new_schema['required'].add(nv)

                for k, v in schema.get('mapping', {}).items():
                    if isinstance(v, dict) and len(v) == 1 and 'from' in v:
                        action, source = tuple(v.items())[0]
                        d_cn, d_var = extract_container_var(k)
                        d_nv = f"{d_cn}.{d_var}"
                        s_cn, s_var = extract_container_var(source)
                        s_nv = f"{s_cn}.{s_var}"

                        if d_nv in schema.get('required',
                                              {}) or d_var in schema.get(
                                                  'required', {}):
                            if s_cn == 'input':
                                if s_var not in outer_schema[
                                        'output'] and s_var not in outer_schema[
                                            'input']:
                                    raise errors.MappingError(d_nv, step)
                            elif s_cn != 'env':
                                new_schema['required'].add(s_nv)

                [
                    new_schema['output'].add(extract_container_var(k)[1])
                    for k in schema.get('output', [])
                ]

            level += 1
            outer_schema = new_schema
        outer_schema['required'] = sorted(list(outer_schema['required']))
        outer_schema['output'] = sorted(list(outer_schema['output']))
        for c in ('required', 'output', 'input'):
            # clean data
            if not outer_schema.get(c):
                outer_schema.pop(c, None)

        return outer_schema

    @staticmethod
    def get(id_or_name, version=None) -> t.Union['Orchestration', str]:
        if is_valid_uuid(id_or_name):
            orch = Orchestration.query.get(id_or_name)
            if orch is None:
                return str(errors.EntityNotFound('Orchestration', id_or_name))
        else:
            if id_or_name:
                query = Orchestration.query.filter_by(
                    name=id_or_name).order_by(Orchestration.version.desc())
                if version:
                    query.filter_by(version=version)
                orch = query.first()
                if orch is None:
                    return f"No orchestration found for '{id_or_name}'" + (
                        f" version '{version}'" if version else None)
            else:
                return "No orchestration specified"
        return orch
示例#11
0
class Transfer(UUIDEntityMixin, EntityReprMixin, db.Model):
    __tablename__ = "L_transfer"

    software_id = db.Column(db.ForeignKey('D_software.id'))
    dest_path = db.Column(db.Text, nullable=False)
    num_chunks = db.Column(db.Integer, nullable=False)
    status = db.Column(typos.Enum(Status),
                       nullable=False,
                       default=Status.WAITING_CHUNKS)
    created_on = db.Column(UtcDateTime(timezone=True))
    started_on = db.Column(UtcDateTime(timezone=True))
    ended_on = db.Column(UtcDateTime(timezone=True))
    _filename = db.Column("filename", db.String(256))
    _size = db.Column("size", db.Integer)
    _checksum = db.Column("checksum", db.Text())

    software = db.relationship("Software", uselist=False)

    def __init__(self,
                 software: t.Union[Software, str],
                 dest_path: str,
                 num_chunks: int,
                 status: Status = None,
                 size: int = None,
                 checksum: str = None,
                 created_on=None,
                 **kwargs):
        super().__init__(**kwargs)
        if isinstance(software, Software):
            self.software = software
        else:
            self._filename = software
            if size is None:
                ValueError("'size' must be specified when sending a file")
            self._size = size
            if checksum is None:
                ValueError("'checksum' must be specified when sending a file")
            self._checksum = checksum
        self.dest_path = dest_path
        self.num_chunks = num_chunks
        self.status = status or Status.WAITING_CHUNKS
        self.created_on = created_on or get_now()

    def to_json(self):
        json = dict(id=str(self.id),
                    dest_path=self.dest_path,
                    num_chunks=self.num_chunks,
                    status=self.status.name,
                    created_on=self.created_on.strftime(
                        dimensigon.defaults.DATETIME_FORMAT),
                    file=os.path.join(self.dest_path, self.filename))

        if self.software:
            json.update(software_id=str(self.software.id))
        else:
            json.update(size=self._size, checksum=self._checksum)

        if self.started_on:
            json.update(started_on=self.started_on.strftime(
                dimensigon.defaults.DATETIME_FORMAT))
        if self.ended_on:
            json.update(ended_on=self.ended_on.strftime(
                dimensigon.defaults.DATETIME_FORMAT))
        return json

    @property
    def filename(self):
        if self.software:
            return self.software.filename
        else:
            return self._filename

    @property
    def size(self):
        if self.software:
            return self.software.size
        else:
            return self._size

    @property
    def checksum(self):
        if self.software:
            return self.software.checksum
        else:
            return self._checksum

    def wait_transfer(self,
                      timeout=None,
                      refresh_interval: float = 0.02) -> Status:
        timeout = timeout or 300
        refresh_interval = refresh_interval
        start = time.time()
        db.session.refresh(self)
        delta = 0
        while self.status in (Status.IN_PROGRESS,
                              Status.WAITING_CHUNKS) and delta < timeout:
            time.sleep(refresh_interval)
            db.session.refresh(self)
            delta = time.time() - start

        return self.status
示例#12
0
class StepExecution(UUIDEntityMixin, EntityReprMixin, db.Model):
    __tablename__ = 'L_step_execution'

    start_time = db.Column(UtcDateTime(timezone=True), nullable=False)
    end_time = db.Column(UtcDateTime(timezone=True))
    params = db.Column(db.JSON)
    rc = db.Column(db.Integer)
    stdout = db.Column(db.Text)
    stderr = db.Column(db.Text)
    success = db.Column(db.Boolean)
    step_id = db.Column(UUID, db.ForeignKey('D_step.id'), nullable=False)
    server_id = db.Column(UUID, db.ForeignKey('D_server.id'))
    orch_execution_id = db.Column(UUID, db.ForeignKey('L_orch_execution.id'))
    pre_process_elapsed_time = db.Column(db.Float)
    execution_elapsed_time = db.Column(db.Float)
    post_process_elapsed_time = db.Column(db.Float)
    child_orch_execution_id = db.Column(UUID)

    step = db.relationship("Step")
    server = db.relationship("Server", foreign_keys=[server_id])
    orch_execution = db.relationship("OrchExecution",
                                     foreign_keys=[orch_execution_id],
                                     uselist=False,
                                     back_populates="step_executions")
    child_orch_execution = db.relationship(
        "OrchExecution",
        uselist=False,
        foreign_keys=[child_orch_execution_id],
        primaryjoin="StepExecution.child_orch_execution_id==OrchExecution.id")

    # def __init__(self, *args, **kwargs):
    #     UUIDEntityMixin.__init__(self, **kwargs)

    def load_completed_result(self, cp: 'CompletedProcess'):
        self.success = cp.success
        self.stdout = cp.stdout
        self.stderr = cp.stderr
        self.rc = cp.rc

    def to_json(self, human=False, split_lines=False):
        data = {}
        if self.id:
            data.update(id=str(self.id))
        if self.start_time:
            data.update(
                start_time=self.start_time.strftime(defaults.DATETIME_FORMAT))
        if self.end_time:
            data.update(
                end_time=self.end_time.strftime(defaults.DATETIME_FORMAT))
        data.update(params=self.params)
        data.update(step_id=str(getattr(self.step, 'id', None)
                                ) if getattr(self.step, 'id', None) else None)
        data.update(rc=self.rc, success=self.success)
        if human:
            data.update(server=str(self.server) if self.server else None)
            try:
                stdout = json.loads(self.stdout)
            except:
                stdout = self.stdout.split(
                    '\n'
                ) if split_lines and self.stdout and '\n' in self.stdout else self.stdout
            try:
                stderr = json.loads(self.stderr)
            except:
                stderr = self.stderr.split(
                    '\n'
                ) if split_lines and self.stderr and '\n' in self.stderr else self.stderr
        else:
            data.update(server_id=str(getattr(self.server, 'id', None))
                        if getattr(self.server, 'id', None) else None)
            stdout = self.stdout.split(
                '\n'
            ) if split_lines and self.stdout and '\n' in self.stdout else self.stdout
            stderr = self.stderr.split(
                '\n'
            ) if split_lines and self.stderr and '\n' in self.stderr else self.stderr
        data.update(stdout=stdout)
        data.update(stderr=stderr)
        if self.child_orch_execution_id:
            data.update(
                child_orch_execution_id=str(self.child_orch_execution_id))
        data.update(pre_process_elapsed_time=self.pre_process_elapsed_time
                    ) if self.pre_process_elapsed_time is not None else None
        data.update(execution_elapsed_time=self.execution_elapsed_time
                    ) if self.execution_elapsed_time is not None else None
        data.update(post_process_elapsed_time=self.post_process_elapsed_time
                    ) if self.post_process_elapsed_time is not None else None
        return data
示例#13
0
class OrchExecution(UUIDEntityMixin, EntityReprMixin, db.Model):
    __tablename__ = 'L_orch_execution'

    start_time = db.Column(UtcDateTime(timezone=True),
                           nullable=False,
                           default=get_now)
    end_time = db.Column(UtcDateTime(timezone=True))
    orchestration_id = db.Column(UUID,
                                 db.ForeignKey('D_orchestration.id'),
                                 nullable=False)
    target = db.Column(db.JSON)
    params = db.Column(db.JSON)
    executor_id = db.Column(UUID, db.ForeignKey('D_user.id'))
    service_id = db.Column(UUID, db.ForeignKey('D_service.id'))
    success = db.Column(db.Boolean)
    undo_success = db.Column(db.Boolean)
    message = db.Column(db.Text)
    server_id = db.Column(UUID, db.ForeignKey('D_server.id'))
    parent_step_execution_id = db.Column(UUID)

    orchestration = db.relationship("Orchestration")
    executor = db.relationship("User")
    service = db.relationship("Service")
    step_executions = db.relationship("StepExecution",
                                      back_populates="orch_execution",
                                      order_by="StepExecution.start_time")

    server = db.relationship("Server", foreign_keys=[server_id])
    parent_step_execution = db.relationship(
        "StepExecution",
        uselist=False,
        foreign_keys=[parent_step_execution_id],
        primaryjoin="OrchExecution.parent_step_execution_id==StepExecution.id")

    # def __init__(self, *args, **kwargs):
    #     UUIDEntityMixin.__init__(self, **kwargs)

    def to_json(self, add_step_exec=False, human=False, split_lines=False):
        data = {}
        if self.id:
            data.update(id=str(self.id))
        if self.start_time:
            data.update(
                start_time=self.start_time.strftime(defaults.DATETIME_FORMAT))
        if self.end_time:
            data.update(
                end_time=self.end_time.strftime(defaults.DATETIME_FORMAT))
        if human:
            # convert target ids to server names
            d = {}
            if isinstance(self.target, dict):
                for k, v in self.target.items():
                    if is_iterable_not_string(v):
                        d[k] = [str(Server.query.get(s) or s) for s in v]
                    else:
                        d[k] = str(Server.query.get(v) or v)
            elif isinstance(self.target, list):
                d = [str(Server.query.get(s) or s) for s in self.target]
            else:
                d = str(Server.query.get(self.target) or self.target)
            data.update(target=d)
            if self.executor:
                data.update(executor=str(self.executor))
            if self.service:
                data.update(service=str(self.service))
            if self.orchestration:
                data.update(
                    orchestration=dict(id=str(self.orchestration.id),
                                       name=self.orchestration.name,
                                       version=self.orchestration.version))
            else:
                data.update(orchestration=None)
            if self.server:
                data.update(
                    server=dict(id=str(self.server.id), name=self.server.name))
        else:
            data.update(target=self.target)
            if self.orchestration_id or getattr(self.orchestration, 'id',
                                                None):
                data.update(
                    orchestration_id=str(self.orchestration_id or getattr(
                        self.orchestration, 'id', None)))
            if self.executor_id or getattr(self.executor, 'id', None):
                data.update(executor_id=str(
                    self.executor_id or getattr(self.executor, 'id', None)))
            if self.service_id or getattr(self.service, 'id', None):
                data.update(service_id=str(
                    self.server_id or getattr(self.service, 'id', None)))
            if self.server_id or getattr(self.server, 'id', None):
                data.update(server_id=str(self.server_id
                                          or getattr(self.server, 'id', None)))
        data.update(params=self.params)
        data.update(success=self.success)
        data.update(undo_success=self.undo_success)
        data.update(message=self.message)

        if self.parent_step_execution_id and not add_step_exec:
            data.update(
                parent_step_execution_id=str(self.parent_step_execution_id))
        if add_step_exec:
            steps = []
            for se in self.step_executions:
                se: StepExecution

                se_json = se.to_json(human, split_lines=split_lines)
                if se.child_orch_execution:
                    se_json[
                        'orch_execution'] = se.child_orch_execution.to_json(
                            add_step_exec=add_step_exec,
                            split_lines=split_lines,
                            human=human)
                elif se.child_orch_execution_id:
                    from dimensigon.web.network import get, Response
                    from dimensigon.network.auth import HTTPBearerAuth
                    from flask_jwt_extended import create_access_token
                    params = ['steps']
                    if human:
                        params.append('human')

                    try:
                        resp = get(se.server,
                                   'api_1_0.orchexecutionresource',
                                   view_data=dict(
                                       execution_id=se.child_orch_execution_id,
                                       params=params))
                    except Exception as e:
                        current_app.logger.exception(
                            f"Exception while trying to acquire orch execution "
                            f"{se.child_orch_execution_id} from {se.server}")
                        resp = Response(exception=e)

                    if resp.ok:
                        se_json['orch_execution'] = resp.msg
                        se_json.pop('child_orch_execution_id', None)

                steps.append(se_json)
            # steps.sort(key=lambda x: x.start_time)
            data.update(steps=steps)
        return data

    @classmethod
    def from_json(cls, kwargs):
        if 'start_time' in kwargs:
            kwargs['start_time'] = datetime.strptime(kwargs.get('start_time'),
                                                     defaults.DATETIME_FORMAT)
        if 'end_time' in kwargs:
            kwargs['end_time'] = datetime.strptime(kwargs.get('end_time'),
                                                   defaults.DATETIME_FORMAT)
        try:
            o = cls.query.get(kwargs.get('id'))
        except RuntimeError as e:
            o = None
        if o:
            for k, v in kwargs.items():
                if getattr(o, k) != v:
                    setattr(o, k, v)
            return o
        else:
            return cls(**kwargs)