Пример #1
0
class CloudSize(me.Document):
    """A base Cloud Size Model."""
    id = me.StringField(primary_key=True, default=lambda: uuid.uuid4().hex)
    cloud = me.ReferenceField('Cloud',
                              required=True,
                              reverse_delete_rule=me.CASCADE)
    external_id = me.StringField(required=True)
    name = me.StringField()
    cpus = me.IntField()
    ram = me.IntField()
    disk = me.IntField()
    bandwidth = me.IntField()
    missing_since = me.DateTimeField()
    extra = MistDictField()  # price info  is included here

    meta = {
        'collection':
        'sizes',
        'indexes': [
            {
                'fields': ['cloud', 'external_id'],
                'sparse': False,
                'unique': True,
                'cls': False,
            },
        ]
    }

    def __str__(self):
        name = "%s, %s (%s)" % (self.name, self.cloud.id, self.external_id)
        return name

    def as_dict(self):
        return {
            'id':
            self.id,
            'cloud':
            self.cloud.id,
            'external_id':
            self.external_id,
            'name':
            self.name,
            'cpus':
            self.cpus,
            'ram':
            self.ram,
            'bandwidth':
            self.bandwidth,
            'extra':
            self.extra,
            'disk':
            self.disk,
            'missing_since':
            str(
                self.missing_since.replace(
                    tzinfo=None) if self.missing_since else '')
        }
Пример #2
0
class CloudLocation(OwnershipMixin, me.Document):
    """A base Cloud Location Model."""
    id = me.StringField(primary_key=True, default=lambda: uuid.uuid4().hex)
    cloud = me.ReferenceField('Cloud',
                              required=True,
                              reverse_delete_rule=me.CASCADE)
    owner = me.ReferenceField('Organization',
                              required=True,
                              reverse_delete_rule=me.CASCADE)
    external_id = me.StringField(required=True)
    name = me.StringField()
    country = me.StringField()
    missing_since = me.DateTimeField()
    extra = MistDictField()

    meta = {
        'collection':
        'locations',
        'indexes': [
            {
                'fields': ['cloud', 'external_id'],
                'sparse': False,
                'unique': True,
                'cls': False,
            },
        ]
    }

    def __str__(self):
        name = "%s, %s (%s)" % (self.name, self.cloud.id, self.external_id)
        return name

    def as_dict(self):
        return {
            'id':
            self.id,
            'extra':
            self.extra,
            'cloud':
            self.cloud.id,
            'external_id':
            self.external_id,
            'name':
            self.name,
            'country':
            self.country,
            'missing_since':
            str(
                self.missing_since.replace(
                    tzinfo=None) if self.missing_since else '')
        }

    def clean(self):
        # Populate owner field based on self.cloud.owner
        if not self.owner:
            self.owner = self.cloud.owner
Пример #3
0
class CloudImage(me.Document):
    """A base Cloud Image Model."""
    id = me.StringField(primary_key=True, default=lambda: uuid.uuid4().hex)
    cloud = me.ReferenceField('Cloud', required=True,
                              reverse_delete_rule=me.CASCADE)
    external_id = me.StringField(required=True)
    name = me.StringField()
    starred = me.BooleanField(default=True)
    stored_after_search = me.BooleanField(default=False)
    missing_since = me.DateTimeField()
    extra = MistDictField()
    os_type = me.StringField(default='linux')
    meta = {
        'collection': 'images',
        'indexes': [
            {
                'fields': ['cloud', 'external_id'],
                'sparse': False,
                'unique': True,
                'cls': False,
            },
        ]
    }

    def __str__(self):
        name = "%s, %s (%s)" % (self.name, self.cloud.id, self.external_id)
        return name

    def as_dict(self):
        return {
            'id': self.id,
            'cloud': self.cloud.id,
            'external_id': self.external_id,
            'name': self.name,
            'starred': self.starred,
            'extra': self.extra,
            'os_type': self.os_type,
            'missing_since': str(self.missing_since.replace(tzinfo=None)
                                 if self.missing_since else '')
        }
Пример #4
0
class Machine(OwnershipMixin, me.Document):
    """The basic machine model"""

    id = me.StringField(primary_key=True, default=lambda: uuid.uuid4().hex)

    cloud = me.ReferenceField('Cloud', required=True,
                              reverse_delete_rule=me.CASCADE)
    owner = me.ReferenceField('Organization', required=True,
                              reverse_delete_rule=me.CASCADE)
    location = me.ReferenceField('CloudLocation', required=False,
                                 reverse_delete_rule=me.DENY)
    size = me.ReferenceField('CloudSize', required=False,
                             reverse_delete_rule=me.DENY)
    image = me.ReferenceField('CloudImage', required=False,
                              reverse_delete_rule=me.DENY)
    network = me.ReferenceField('Network', required=False,
                                reverse_delete_rule=me.NULLIFY)
    subnet = me.ReferenceField('Subnet', required=False,
                               reverse_delete_rule=me.NULLIFY)
    name = me.StringField()

    # Info gathered mostly by libcloud (or in some cases user input).
    # Be more specific about what this is.
    # We should perhaps come up with a better name.
    machine_id = me.StringField(required=True)
    hostname = me.StringField()
    public_ips = me.ListField()
    private_ips = me.ListField()
    ssh_port = me.IntField(default=22)
    OS_TYPES = ('windows', 'coreos', 'freebsd', 'linux', 'unix')
    os_type = me.StringField(default='unix', choices=OS_TYPES)
    rdp_port = me.IntField(default=3389)
    actions = me.EmbeddedDocumentField(Actions, default=lambda: Actions())
    extra = MistDictField()
    cost = me.EmbeddedDocumentField(Cost, default=lambda: Cost())
    # libcloud.compute.types.NodeState
    state = me.StringField(default='unknown',
                           choices=tuple(config.STATES.values()))
    machine_type = me.StringField(default='machine',
                                  choices=('machine', 'vm', 'container',
                                           'hypervisor', 'container-host',
                                           'ilo-host'))
    parent = me.ReferenceField('Machine', required=False,
                               reverse_delete_rule=me.NULLIFY)

    # Deprecated TODO: Remove in v5
    key_associations = me.EmbeddedDocumentListField(KeyAssociation)

    last_seen = me.DateTimeField()
    missing_since = me.DateTimeField()
    unreachable_since = me.DateTimeField()
    created = me.DateTimeField()

    monitoring = me.EmbeddedDocumentField(Monitoring,
                                          default=lambda: Monitoring())

    ssh_probe = me.EmbeddedDocumentField(SSHProbe, required=False)
    ping_probe = me.EmbeddedDocumentField(PingProbe, required=False)

    expiration = me.ReferenceField(Schedule, required=False,
                                   reverse_delete_rule=me.NULLIFY)

    # Number of vCPUs gathered from various sources. This field is meant to
    # be updated ONLY by the mist.api.metering.tasks:find_machine_cores task.
    cores = me.IntField()

    meta = {
        'collection': 'machines',
        'indexes': [
            {
                'fields': [
                    'cloud',
                    'machine_id'
                ],
                'sparse': False,
                'unique': True,
                'cls': False,
            }, {
                'fields': [
                    'monitoring.installation_status.activated_at'
                ],
                'sparse': True,
                'unique': False
            }
        ],
        'strict': False,
    }

    def __init__(self, *args, **kwargs):
        super(Machine, self).__init__(*args, **kwargs)
        self.ctl = MachineController(self)

    def clean(self):
        # Remove any KeyAssociation, whose `keypair` has been deleted. Do NOT
        # perform an atomic update on self, but rather remove items from the
        # self.key_associations list by iterating over it and popping matched
        # embedded documents in order to ensure that the most recent list is
        # always processed and saved.
        key_associations = KeyMachineAssociation.objects(machine=self)
        for ka in reversed(list(range(len(key_associations)))):
            if key_associations[ka].key.deleted:
                key_associations[ka].delete()

        # Reset key_associations in case self goes missing/destroyed. This is
        # going to prevent the machine from showing up as "missing" in the
        # corresponding keys' associated machines list.
        if self.missing_since:
            self.key_associations = []

        # Populate owner field based on self.cloud.owner
        if not self.owner:
            self.owner = self.cloud.owner

        self.clean_os_type()

        if self.monitoring.method not in config.MONITORING_METHODS:
            self.monitoring.method = config.DEFAULT_MONITORING_METHOD

    def clean_os_type(self):
        """Clean self.os_type"""
        if self.os_type not in self.OS_TYPES:
            for os_type in self.OS_TYPES:
                if self.os_type.lower() == os_type:
                    self.os_type = os_type
                    break
            else:
                self.os_type = 'unix'

    def delete(self):
        if self.expiration:
            self.expiration.delete()
        super(Machine, self).delete()
        mist.api.tag.models.Tag.objects(
            resource_id=self.id, resource_type='machine').delete()
        try:
            self.owner.mapper.remove(self)
        except (AttributeError, me.DoesNotExist) as exc:
            log.error(exc)
        try:
            if self.owned_by:
                self.owned_by.get_ownership_mapper(self.owner).remove(self)
        except (AttributeError, me.DoesNotExist) as exc:
            log.error(exc)

    def as_dict(self):
        # Return a dict as it will be returned to the API
        tags = {tag.key: tag.value for tag in mist.api.tag.models.Tag.objects(
            resource_id=self.id, resource_type='machine'
        ).only('key', 'value')}
        try:
            if self.expiration:
                expiration = {
                    'id': self.expiration.id,
                    'action': self.expiration.task_type.action,
                    'date': self.expiration.schedule_type.entry.isoformat(),
                    'notify': self.expiration.reminder and int((
                        self.expiration.schedule_type.entry -
                        self.expiration.reminder.schedule_type.entry
                    ).total_seconds()) or 0,
                }
            else:
                expiration = None
        except Exception as exc:
            log.error("Error getting expiration for machine %s: %r" % (
                self.id, exc))
            self.expiration = None
            self.save()
            expiration = None

        try:
            from bson import json_util
            extra = json.loads(json.dumps(self.extra,
                                          default=json_util.default))
        except Exception as exc:
            log.error('Failed to serialize extra metadata for %s: %s\n%s' % (
                self, self.extra, exc))
            extra = {}

        return {
            'id': self.id,
            'hostname': self.hostname,
            'public_ips': self.public_ips,
            'private_ips': self.private_ips,
            'name': self.name,
            'ssh_port': self.ssh_port,
            'os_type': self.os_type,
            'rdp_port': self.rdp_port,
            'machine_id': self.machine_id,
            'actions': {action: self.actions[action]
                        for action in self.actions},
            'extra': extra,
            'cost': self.cost.as_dict(),
            'state': self.state,
            'tags': tags,
            'monitoring':
                self.monitoring.as_dict() if self.monitoring and
                self.monitoring.hasmonitoring else '',
            'key_associations':
                [ka.as_dict() for ka in KeyMachineAssociation.objects(
                    machine=self)],
            'cloud': self.cloud.id,
            'location': self.location.id if self.location else '',
            'size': self.size.name if self.size else '',
            'image': self.image.id if self.image else '',
            'cloud_title': self.cloud.title,
            'last_seen': str(self.last_seen.replace(tzinfo=None)
                             if self.last_seen else ''),
            'missing_since': str(self.missing_since.replace(tzinfo=None)
                                 if self.missing_since else ''),
            'unreachable_since': str(
                self.unreachable_since.replace(tzinfo=None)
                if self.unreachable_since else ''),
            'created': str(self.created.replace(tzinfo=None)
                           if self.created else ''),
            'machine_type': self.machine_type,
            'parent': self.parent.id if self.parent is not None else '',
            'probe': {
                'ping': (self.ping_probe.as_dict()
                         if self.ping_probe is not None
                         else PingProbe().as_dict()),
                'ssh': (self.ssh_probe.as_dict()
                        if self.ssh_probe is not None
                        else SSHProbe().as_dict()),
            },
            'cores': self.cores,
            'network': self.network.id if self.network else '',
            'subnet': self.subnet.id if self.subnet else '',
            'owned_by': self.owned_by.id if self.owned_by else '',
            'created_by': self.created_by.id if self.created_by else '',
            'expiration': expiration,
            'provider': self.cloud.ctl.provider
        }

    def __str__(self):
        return 'Machine %s (%s) in %s' % (self.name, self.id, self.cloud)
Пример #5
0
class Zone(OwnershipMixin, me.Document):
    """This is the class definition for the Mongo Engine Document related to a
    DNS zone.
    """

    id = me.StringField(primary_key=True, default=lambda: uuid.uuid4().hex)
    owner = me.ReferenceField('Organization', required=True,
                              reverse_delete_rule=me.CASCADE)

    zone_id = me.StringField(required=True)
    domain = me.StringField(required=True)
    type = me.StringField(required=True)
    ttl = me.IntField(required=True, default=0)
    extra = MistDictField()
    cloud = me.ReferenceField(Cloud, required=True,
                              reverse_delete_rule=me.CASCADE)

    deleted = me.DateTimeField()

    meta = {
        'collection': 'zones',
        'indexes': [
            'owner',
            {
                'fields': ['cloud', 'zone_id', 'deleted'],
                'sparse': False,
                'unique': True,
                'cls': False,
            }
        ],
    }

    def __init__(self, *args, **kwargs):
        super(Zone, self).__init__(*args, **kwargs)
        self.ctl = ZoneController(self)

    @classmethod
    def add(cls, owner, cloud, id='', **kwargs):
        """Add Zone

        This is a class method, meaning that it is meant to be called on the
        class itself and not on an instance of the class.

        You're not meant to be calling this directly, but on a cloud subclass
        instead like this:

            zone = Zone.add(owner=org, domain='domain.com.')

        Params:
        - owner and domain are common and required params
        - only provide a custom zone id if you're migrating something
        - kwargs will be passed to appropriate controller, in most cases these
          should match the extra fields of the particular zone type.

        """
        if not kwargs['domain']:
            raise RequiredParameterMissingError('domain')
        if not cloud or not isinstance(cloud, Cloud):
            raise BadRequestError('cloud')
        if not owner or not isinstance(owner, Organization):
            raise BadRequestError('owner')
        zone = cls(owner=owner, cloud=cloud, domain=kwargs['domain'])
        if id:
            zone.id = id
        return zone.ctl.create_zone(**kwargs)

    def delete(self):
        super(Zone, self).delete()
        Tag.objects(resource_id=self.id, resource_type='zone').delete()
        self.owner.mapper.remove(self)
        if self.owned_by:
            self.owned_by.get_ownership_mapper(self.owner).remove(self)

    @property
    def tags(self):
        """Return the tags of this zone."""
        return {tag.key: tag.value
                for tag in Tag.objects(
                    resource_id=self.id, resource_type='zone')}

    def as_dict(self):
        """Return a dict with the model values."""
        return {
            'id': self.id,
            'zone_id': self.zone_id,
            'domain': self.domain,
            'type': self.type,
            'ttl': self.ttl,
            'extra': self.extra,
            'cloud': self.cloud.id,
            'owned_by': self.owned_by.id if self.owned_by else '',
            'created_by': self.created_by.id if self.created_by else '',
            'records': {r.id: r.as_dict() for r
                        in Record.objects(zone=self, deleted=None)},
            'tags': self.tags
        }

    def clean(self):
        """Overriding the default clean method to implement param checking"""
        if not self.domain.endswith('.'):
            self.domain += "."

    def __str__(self):
        return 'Zone %s (%s/%s) of %s' % (self.id, self.zone_id, self.domain,
                                          self.owner)
Пример #6
0
class Record(OwnershipMixin, me.Document):
    """This is the class definition for the Mongo Engine Document related to a
    DNS record.
    """

    id = me.StringField(primary_key=True, default=lambda: uuid.uuid4().hex)

    record_id = me.StringField(required=True)
    name = me.StringField(required=True)
    type = me.StringField(required=True)
    rdata = me.ListField(required=True)
    extra = MistDictField()
    ttl = me.IntField(default=0)
    # This ensures that any records that are under a zone are also deleted when
    # we delete the zone.
    zone = me.ReferenceField(Zone, required=True,
                             reverse_delete_rule=me.CASCADE)
    owner = me.ReferenceField('Organization', required=True,
                              reverse_delete_rule=me.CASCADE)

    deleted = me.DateTimeField()

    meta = {
        'collection': 'records',
        'allow_inheritance': True,
        'indexes': [
            {
                'fields': ['zone', 'record_id', 'deleted'],
                'sparse': False,
                'unique': True,
                'cls': False,
            }
        ],
    }
    _record_type = None

    def __init__(self, *args, **kwargs):
        super(Record, self).__init__(*args, **kwargs)
        self.ctl = RecordController(self)

    @classmethod
    def add(cls, owner=None, zone=None, id='', **kwargs):
        """Add Record

        This is a class method, meaning that it is meant to be called on the
        class itself and not on an instance of the class.

        You're not meant to be calling this directly, but on a cloud subclass
        instead like this:

            record = Record.add(zone=zone, **kwargs)

        Params:
        - zone is a required param
        - only provide a custom record id if you're migrating something
        - kwargs will be passed to appropriate controller, in most cases these
          should match the extra fields of the particular record type.

        """
        if not kwargs['name']:
            raise RequiredParameterMissingError('name')
        if not kwargs['data']:
            raise RequiredParameterMissingError('data')
        if not kwargs['type']:
            raise RequiredParameterMissingError('type')
        # If we were not given a zone then we need the owner to try and find
        # the best matching domain.
        if not zone and kwargs['type'] in ['A', 'AAAA', 'CNAME']:
            assert isinstance(owner, Organization)
            zone = BaseDNSController.find_best_matching_zone(owner,
                                                             kwargs['name'])
        assert isinstance(zone, Zone)

        record = cls(zone=zone)
        if id:
            record.id = id
        return record.ctl.create_record(**kwargs)

    def delete(self):
        super(Record, self).delete()
        Tag.objects(resource_id=self.id, resource_type='record').delete()
        self.zone.owner.mapper.remove(self)
        if self.owned_by:
            self.owned_by.get_ownership_mapper(self.owner).remove(self)

    def clean(self):
        """Overriding the default clean method to implement param checking"""
        self.type = self._record_type
        if not self.owner:
            self.owner = self.zone.owner

    def __str__(self):
        return 'Record %s (name:%s, type:%s) of %s' % (
            self.id, self.name, self.type, self.zone.domain)

    @property
    def tags(self):
        """Return the tags of this record."""
        return {tag.key: tag.value
                for tag in Tag.objects(resource_id=self.id,
                                       resource_type='record')}

    def as_dict(self):
        """ Return a dict with the model values."""
        return {
            'id': self.id,
            'record_id': self.record_id,
            'name': self.name,
            'type': self.type,
            'rdata': self.rdata,
            'ttl': self.ttl,
            'extra': self.extra,
            'zone': self.zone.id,
            'owned_by': self.owned_by.id if self.owned_by else '',
            'created_by': self.created_by.id if self.created_by else '',
            'tags': self.tags
        }
Пример #7
0
class Network(OwnershipMixin, me.Document):
    """The basic Network model.

    This class is only meant to be used as a basic class for cloud-specific
    `Network` subclasses.

    `Network` contains all common, provider-independent fields and handlers.
    """

    id = me.StringField(primary_key=True, default=lambda: uuid.uuid4().hex)
    owner = me.ReferenceField('Organization', reverse_delete_rule=me.CASCADE)
    cloud = me.ReferenceField(Cloud,
                              required=True,
                              reverse_delete_rule=me.CASCADE)
    network_id = me.StringField()  # required=True)

    name = me.StringField()
    cidr = me.StringField()
    description = me.StringField()
    location = me.ReferenceField('CloudLocation',
                                 required=False,
                                 reverse_delete_rule=me.DENY)

    extra = MistDictField()  # The `extra` dictionary returned by libcloud.

    missing_since = me.DateTimeField()

    meta = {
        'allow_inheritance':
        True,
        'collection':
        'networks',
        'indexes': [
            {
                'fields': ['cloud', 'network_id'],
                'sparse': False,
                'unique': True,
                'cls': False,
            },
        ],
    }

    def __init__(self, *args, **kwargs):
        super(Network, self).__init__(*args, **kwargs)
        # Set `ctl` attribute.
        self.ctl = NetworkController(self)
        # Calculate and store network type specific fields.
        self._network_specific_fields = [
            field for field in type(self)._fields
            if field not in Network._fields
        ]

    @classmethod
    def add(cls, cloud, cidr=None, name='', description='', id='', **kwargs):
        """Add a Network.

        This is a class method, meaning that it is meant to be called on the
        class itself and not on an instance of the class.

        You're not meant to be calling this directly, but on a network subclass
        instead like this:

            network = AmazonNetwork.add(cloud=cloud, name='Ec2Network')

        :param cloud: the Cloud on which the network is going to be created.
        :param cidr:
        :param name: the name to be assigned to the new network.
        :param description: an optional description.
        :param id: a custom object id, passed in case of a migration.
        :param kwargs: the kwargs to be passed to the corresponding controller.

        """
        assert isinstance(cloud, Cloud)
        network = cls(cloud=cloud,
                      cidr=cidr,
                      name=name,
                      description=description)
        if id:
            network.id = id
        return network.ctl.create(**kwargs)

    @property
    def tags(self):
        """Return the tags of this network."""
        return {
            tag.key: tag.value
            for tag in Tag.objects(resource_id=self.id,
                                   resource_type='network')
        }

    def clean(self):
        """Checks the CIDR to determine if it maps to a valid IPv4 network."""
        if self.cidr:
            try:
                netaddr.cidr_to_glob(self.cidr)
            except (TypeError, netaddr.AddrFormatError) as err:
                raise me.ValidationError(err)
        self.owner = self.owner or self.cloud.owner

    def delete(self):
        super(Network, self).delete()
        self.owner.mapper.remove(self)
        Tag.objects(resource_id=self.id, resource_type='network').delete()
        if self.owned_by:
            self.owned_by.get_ownership_mapper(self.owner).remove(self)

    def as_dict(self):
        """Returns the API representation of the `Network` object."""
        net_dict = {
            'id': self.id,
            'subnets': {
                s.id: s.as_dict()
                for s in Subnet.objects(network=self, missing_since=None)
            },
            'cloud': self.cloud.id,
            'network_id': self.network_id,
            'name': self.name,
            'cidr': self.cidr,
            'description': self.description,
            'extra': self.extra,
            'tags': self.tags,
            'owned_by': self.owned_by.id if self.owned_by else '',
            'created_by': self.created_by.id if self.created_by else '',
            'location': self.location.id if self.location else '',
        }
        net_dict.update(
            {key: getattr(self, key)
             for key in self._network_specific_fields})
        return net_dict

    def __str__(self):
        return '%s "%s" (%s)' % (self.__class__.__name__, self.name, self.id)
Пример #8
0
class Subnet(me.Document):
    """The basic Subnet model.

    This class is only meant to be used as a basic class for cloud-specific
    `Subnet` subclasses.

    `Subnet` contains all common, provider-independent fields and handlers.
    """

    id = me.StringField(primary_key=True, default=lambda: uuid.uuid4().hex)
    owner = me.ReferenceField('Organization', reverse_delete_rule=me.CASCADE)
    network = me.ReferenceField('Network',
                                required=True,
                                reverse_delete_rule=me.CASCADE)
    subnet_id = me.StringField()

    name = me.StringField()
    cidr = me.StringField(required=True)
    description = me.StringField()

    extra = MistDictField()  # The `extra` dictionary returned by libcloud.

    missing_since = me.DateTimeField()

    meta = {
        'allow_inheritance':
        True,
        'collection':
        'subnets',
        'indexes': [
            {
                'fields': ['network', 'subnet_id'],
                'sparse': False,
                'unique': True,
                'cls': False,
            },
        ],
    }

    def __init__(self, *args, **kwargs):
        super(Subnet, self).__init__(*args, **kwargs)
        # Set `ctl` attribute.
        self.ctl = SubnetController(self)
        # Calculate and store subnet type specific fields.
        self._subnet_specific_fields = [
            field for field in type(self)._fields
            if field not in Subnet._fields
        ]

    @classmethod
    def add(cls, network, cidr, name='', description='', id='', **kwargs):
        """Add a Subnet.

        This is a class method, meaning that it is meant to be called on the
        class itself and not on an instance of the class.

        You're not meant to be calling this directly, but on a network subclass
        instead like this:

            subnet = AmazonSubnet.add(network=network,
                                      name='Ec2Subnet',
                                      cidr='172.31.10.0/24')

        :param network: the Network nn which the subnet is going to be created.
        :param cidr: the CIDR to be assigned to the new subnet.
        :param name: the name to be assigned to the new subnet.
        :param description: an optional description.
        :param id: a custom object id, passed in case of a migration.
        :param kwargs: the kwargs to be passed to the corresponding controller.

        """
        assert isinstance(network, Network)
        if not cidr:
            raise RequiredParameterMissingError('cidr')
        subnet = cls(network=network,
                     cidr=cidr,
                     name=name,
                     description=description)
        if id:
            subnet.id = id
        return subnet.ctl.create(**kwargs)

    @property
    def tags(self):
        """Return the tags of this subnet."""
        return {
            tag.key: tag.value
            for tag in Tag.objects(resource_id=self.id, resource_type='subnet')
        }

    def clean(self):
        """Checks the CIDR to determine if it maps to a valid IPv4 network."""
        self.owner = self.owner or self.network.cloud.owner
        try:
            netaddr.cidr_to_glob(self.cidr)
        except (TypeError, netaddr.AddrFormatError) as err:
            raise me.ValidationError(err)

    def delete(self):
        super(Subnet, self).delete()
        Tag.objects(resource_id=self.id, resource_type='subnet').delete()

    def as_dict(self):
        """Returns the API representation of the `Subnet` object."""
        subnet_dict = {
            'id': self.id,
            'cloud': self.network.cloud.id,
            'network': self.network.id,
            'subnet_id': self.subnet_id,
            'name': self.name,
            'cidr': self.cidr,
            'description': self.description,
            'extra': self.extra,
            'tags': self.tags,
        }
        subnet_dict.update(
            {key: getattr(self, key)
             for key in self._subnet_specific_fields})
        return subnet_dict

    def __str__(self):
        return '%s "%s" (%s)' % (self.__class__.__name__, self.name, self.id)