Merge "Extend AssignMachinesIP,fix Machine define"
diff --git a/README.rst b/README.rst
index 71c356d..fdfbb38 100644
--- a/README.rst
+++ b/README.rst
@@ -46,14 +46,16 @@
       user: mirantis
       token: "89EgtWkX45ddjMYpuL:SqVjxFG87Dr6kVf4Wp:5WLfbUgmm9XQtJxm3V2LUUy7bpCmqmnk"
       fabrics:
-        test-fabric:
-          description: Test fabric
+        test-fabric1:
+          description: "Test fabric"
+        test-fabric2:
+          description: "Test fabric2"
       subnets:
         subnet1:
-          fabric: test-fabric
+          fabric: test-fabric1
           cidr: 2.2.3.0/24
           gateway_ip: 2.2.3.2
-          iprange:
+          iprange: # reserved range for DHCP\auto mapping
             start: 2.2.3.20
             end: 2.2.3.250
       dhcp_snippets:
@@ -80,38 +82,29 @@
                Version: GnuPG v2
 
                mQENBFOpvpgBCADkP656H41i8fpplEEB8IeLhugyC2rTEwwSclb8tQNYtUiGdna9
-               m38kb0OS2DDrEdtdQb2hWCnswxaAkUunb2qq18vd3dBvlnI+C4/xu5ksZZkRj+fW
-               tArNR18V+2jkwcG26m8AxIrT+m4M6/bgnSfHTBtT5adNfVcTHqiT1JtCbQcXmwVw
-               WbqS6v/LhcsBE//SHne4uBCK/GHxZHhQ5jz5h+3vWeV4gvxS3Xu6v1IlIpLDwUts
-               kT1DumfynYnnZmWTGc6SYyIFXTPJLtnoWDb9OBdWgZxXfHEcBsKGha+bXO+m2tHA
-               gNneN9i5f8oNxo5njrL8jkCckOpNpng18BKXABEBAAG0MlNhbHRTdGFjayBQYWNr
-               YWdpbmcgVGVhbSA8cGFja2FnaW5nQHNhbHRzdGFjay5jb20+iQE4BBMBAgAiBQJT
-               qb6YAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRAOCKFJ3le/vhkqB/0Q
-               WzELZf4d87WApzolLG+zpsJKtt/ueXL1W1KA7JILhXB1uyvVORt8uA9FjmE083o1
-               yE66wCya7V8hjNn2lkLXboOUd1UTErlRg1GYbIt++VPscTxHxwpjDGxDB1/fiX2o
-               nK5SEpuj4IeIPJVE/uLNAwZyfX8DArLVJ5h8lknwiHlQLGlnOu9ulEAejwAKt9CU
-               4oYTszYM4xrbtjB/fR+mPnYh2fBoQO4d/NQiejIEyd9IEEMd/03AJQBuMux62tjA
-               /NwvQ9eqNgLw9NisFNHRWtP4jhAOsshv1WW+zPzu3ozoO+lLHixUIz7fqRk38q8Q
-               9oNR31KvrkSNrFbA3D89uQENBFOpvpgBCADJ79iH10AfAfpTBEQwa6vzUI3Eltqb
-               9aZ0xbZV8V/8pnuU7rqM7Z+nJgldibFk4gFG2bHCG1C5aEH/FmcOMvTKDhJSFQUx
-               uhgxttMArXm2c22OSy1hpsnVG68G32Nag/QFEJ++3hNnbyGZpHnPiYgej3FrerQJ
-               zv456wIsxRDMvJ1NZQB3twoCqwapC6FJE2hukSdWB5yCYpWlZJXBKzlYz/gwD/Fr
-               GL578WrLhKw3UvnJmlpqQaDKwmV2s7MsoZogC6wkHE92kGPG2GmoRD3ALjmCvN1E
-               PsIsQGnwpcXsRpYVCoW7e2nW4wUf7IkFZ94yOCmUq6WreWI4NggRcFC5ABEBAAGJ
-               AR8EGAECAAkFAlOpvpgCGwwACgkQDgihSd5Xv74/NggA08kEdBkiWWwJZUZEy7cK
-               WWcgjnRuOHd4rPeT+vQbOWGu6x4bxuVf9aTiYkf7ZjVF2lPn97EXOEGFWPZeZbH4
-               vdRFH9jMtP+rrLt6+3c9j0M8SIJYwBL1+CNpEC/BuHj/Ra/cmnG5ZNhYebm76h5f
-               T9iPW9fFww36FzFka4VPlvA4oB7ebBtquFg3sdQNU/MmTVV4jPFWXxh4oRDDR+8N
-               1bcPnbB11b5ary99F/mqr7RgQ+YFF0uKRE3SKa7a+6cIuHEZ7Za+zhPaQlzAOZlx
+                ......
                fuBmScum8uQTrEF5+Um5zkwC7EXTdH1co/+/V/fpOtxIg4XO4kcugZefVm5ERfVS
                MA==
                =dtMN
                -----END PGP PUBLIC KEY BLOCK-----"
           enabled: true
       machines:
-        machine1:
-          interface:
-            mac: "11:22:33:44:55:66"
+        machine1_new_schema:
+          pxe_interface_mac: "11:22:33:44:55:66" # Node will be identified by those mac
+          interfaces:
+            nic01: # could be any, used for iterate only
+              type: eth # NotImplemented
+              name: eth0 # Override default nic name. Interface to rename will be identified by mac
+              mac: "11:22:33:44:55:66"
+              mode: "static"
+              ip: "2.2.3.19"  # ip should be out of reserved subnet range, but still in subnet range
+              subnet: "subnet1"
+              gateway: "2.2.3.2" # override default gateway from subnet
+            nic02:
+              type: eth # Not-implemented
+              mac: "11:22:33:44:55:78"
+              subnet: "subnet2"
+              mode: "dhcp"
           power_parameters:
             power_type: ipmi
             power_address: '192.168.10.10'
@@ -119,6 +112,23 @@
             power_password: bmc_password
             #Optional (for legacy HW)
             power_driver: LAN
+          distro_series: xenial
+          hwe_kernel: hwe-16.04
+        machine1_old_schema:
+          interface:
+              mac: "11:22:33:44:55:88"  # Node will be identified by those mac
+              mode: "static"
+              ip: "2.2.3.15"
+              subnet: "subnet1"
+              gateway: "2.2.3.2"
+          power_parameters:
+            power_type: ipmi
+            power_address: '192.168.10.10'
+            power_user: bmc_user
+            power_password: bmc_password
+            #Optional (for legacy HW)
+            power_driver: LAN
+            # FIXME: that's should be moved into another,livirt example.
             # Used in case of power_type: virsh
             power_id: my_libvirt_vm_name
           distro_series: xenial
@@ -150,7 +160,7 @@
         enable_http_proxy: true
         default_min_hwe_kernel: ''
        sshprefs:
-        - 'ssh-rsa ASDFOSADFISdfasdfasjdklfjasdJFASDJfASdf923@AAAAB3NzaC1yc2EAAAADAQABAAACAQCv8ISOESGgYUOycYw1SAs/SfHTqtSCTephD/7o2+mEZO53xN98sChiFscFaPA2ZSMoZbJ6MQLKcWKMK2OaTdNSAvn4UE4T6VP0ccdumHDNRwO3f6LptvXr9NR5Wocz2KAgptk+uaA8ytM0Aj9NT0UlfjAXkKnoKyNq6yG+lx4HpwolVaFSlqRXf/iuHpCrspv/u1NW7ReMElJoXv+0zZ7Ow0ZylISdYkaqbV8QatCb17v1+xX03xLsZigfugce/8CDsibSYvJv+Hli5CCBsKgfFqLy4R5vGxiLSVzG/asdjalskjdlkasjdasd/asdajsdkjalaksdjfasd/fa/sdf/asd/fas/dfsadf blah@blah'
+        - 'ssh-rsa ASD.........dfsadf blah@blah'
 
 
 
@@ -169,29 +179,7 @@
         Version: GnuPG v2
 
         mQENBFOpvpgBCADkP656H41i8fpplEEB8IeLhugyC2rTEwwSclb8tQNYtUiGdna9
-        m38kb0OS2DDrEdtdQb2hWCnswxaAkUunb2qq18vd3dBvlnI+C4/xu5ksZZkRj+fW
-        tArNR18V+2jkwcG26m8AxIrT+m4M6/bgnSfHTBtT5adNfVcTHqiT1JtCbQcXmwVw
-        WbqS6v/LhcsBE//SHne4uBCK/GHxZHhQ5jz5h+3vWeV4gvxS3Xu6v1IlIpLDwUts
-        kT1DumfynYnnZmWTGc6SYyIFXTPJLtnoWDb9OBdWgZxXfHEcBsKGha+bXO+m2tHA
-        gNneN9i5f8oNxo5njrL8jkCckOpNpng18BKXABEBAAG0MlNhbHRTdGFjayBQYWNr
-        YWdpbmcgVGVhbSA8cGFja2FnaW5nQHNhbHRzdGFjay5jb20+iQE4BBMBAgAiBQJT
-        qb6YAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRAOCKFJ3le/vhkqB/0Q
-        WzELZf4d87WApzolLG+zpsJKtt/ueXL1W1KA7JILhXB1uyvVORt8uA9FjmE083o1
-        yE66wCya7V8hjNn2lkLXboOUd1UTErlRg1GYbIt++VPscTxHxwpjDGxDB1/fiX2o
-        nK5SEpuj4IeIPJVE/uLNAwZyfX8DArLVJ5h8lknwiHlQLGlnOu9ulEAejwAKt9CU
-        4oYTszYM4xrbtjB/fR+mPnYh2fBoQO4d/NQiejIEyd9IEEMd/03AJQBuMux62tjA
-        /NwvQ9eqNgLw9NisFNHRWtP4jhAOsshv1WW+zPzu3ozoO+lLHixUIz7fqRk38q8Q
-        9oNR31KvrkSNrFbA3D89uQENBFOpvpgBCADJ79iH10AfAfpTBEQwa6vzUI3Eltqb
-        9aZ0xbZV8V/8pnuU7rqM7Z+nJgldibFk4gFG2bHCG1C5aEH/FmcOMvTKDhJSFQUx
-        uhgxttMArXm2c22OSy1hpsnVG68G32Nag/QFEJ++3hNnbyGZpHnPiYgej3FrerQJ
-        zv456wIsxRDMvJ1NZQB3twoCqwapC6FJE2hukSdWB5yCYpWlZJXBKzlYz/gwD/Fr
-        GL578WrLhKw3UvnJmlpqQaDKwmV2s7MsoZogC6wkHE92kGPG2GmoRD3ALjmCvN1E
-        PsIsQGnwpcXsRpYVCoW7e2nW4wUf7IkFZ94yOCmUq6WreWI4NggRcFC5ABEBAAGJ
-        AR8EGAECAAkFAlOpvpgCGwwACgkQDgihSd5Xv74/NggA08kEdBkiWWwJZUZEy7cK
-        WWcgjnRuOHd4rPeT+vQbOWGu6x4bxuVf9aTiYkf7ZjVF2lPn97EXOEGFWPZeZbH4
-        vdRFH9jMtP+rrLt6+3c9j0M8SIJYwBL1+CNpEC/BuHj/Ra/cmnG5ZNhYebm76h5f
-        T9iPW9fFww36FzFka4VPlvA4oB7ebBtquFg3sdQNU/MmTVV4jPFWXxh4oRDDR+8N
-        1bcPnbB11b5ary99F/mqr7RgQ+YFF0uKRE3SKa7a+6cIuHEZ7Za+zhPaQlzAOZlx
+        .....
         fuBmScum8uQTrEF5+Um5zkwC7EXTdH1co/+/V/fpOtxIg4XO4kcugZefVm5ERfVS
         MA==
         =dtMN
@@ -269,7 +257,17 @@
         - cmd: maas_login_admin
       ...
 
-List of avaibled `req_status` defined in global variable:
+List of available `req_status` defined in global variable:
+
+.. code-block:: python
+
+    STATUS_NAME_DICT = dict([
+        (0, 'New'), (1, 'Commissioning'), (2, 'Failed commissioning'),
+        (3, 'Missing'), (4, 'Ready'), (5, 'Reserved'), (10, 'Allocated'),
+        (9, 'Deploying'), (6, 'Deployed'), (7, 'Retired'), (8, 'Broken'),
+        (11, 'Failed deployment'), (12, 'Releasing'),
+        (13, 'Releasing failed'), (14, 'Disk erasing'),
+        (15, 'Failed disk erasing')])
 
 
 Read more
diff --git a/_modules/maas.py b/_modules/maas.py
index 1df4698..ee110c8 100644
--- a/_modules/maas.py
+++ b/_modules/maas.py
@@ -100,6 +100,9 @@
                                 None, **data).read()
 
     def process(self, objects_name=None):
+        # FIXME: probably, should be extended with "skipped" return.
+        # For example, currently "DEPLOYED" nodes are skipped, and no changes
+        # applied - but they fall into 'updated' list.
         ret = {
             'success': [],
             'errors': {},
@@ -151,6 +154,8 @@
                         self.send(data)
                         ret['success'].append(name)
                 except urllib2.HTTPError as e:
+                    # FIXME add exception's for response:
+                    # '{"mode": ["Interface is already set to DHCP."]}
                     etxt = e.read()
                     LOG.error('Failed for object %s reason %s', name, etxt)
                     ret['errors'][name] = str(etxt)
@@ -368,10 +373,19 @@
 
     def fill_data(self, name, machine_data):
         power_data = machine_data['power_parameters']
+        machine_pxe_mac = machine_data.get('pxe_interface_mac', None)
+        if machine_data.get("interface", None):
+            LOG.warning(
+                "Old machine-describe detected! "
+                "Please read documentation for "
+                "'salt-formulas/maas' for migration!")
+            machine_pxe_mac = machine_data['interface'].get('mac', None)
+        if not machine_pxe_mac:
+            raise Exception("PXE MAC for machine:{} not defined".format(name))
         data = {
             'hostname': name,
             'architecture': machine_data.get('architecture', 'amd64/generic'),
-            'mac_addresses': machine_data['interface']['mac'],
+            'mac_addresses': machine_pxe_mac,
             'power_type': machine_data.get('power_type', 'ipmi'),
             'power_parameters_power_address': power_data['power_address'],
         }
@@ -399,6 +413,7 @@
 
 
 class AssignMachinesIP(MaasObject):
+    # FIXME
     READY = 4
     DEPLOYED = 6
 
@@ -414,27 +429,153 @@
         self._extra_data_urls = {'machines': (u'api/2.0/machines/',
                                               None, 'hostname')}
 
-    def fill_data(self, name, data, machines):
-        interface = data['interface']
-        machine = machines[name]
-        if machine['status'] == self.DEPLOYED:
-            return
-        if machine['status'] != self.READY:
-            raise Exception('Machine:{} not in READY state'.format(name))
-        if 'ip' not in interface:
-            return
+    def _data_old(self, _interface, _machine):
+        """
+        _interface = {
+            "mac": "11:22:33:44:55:77",
+            "mode": "STATIC",
+            "ip": "2.2.3.15",
+            "subnet": "subnet1",
+            "gateway": "2.2.3.2",
+        }
+        :param data:
+        :return:
+        """
         data = {
             'mode': 'STATIC',
-            'subnet': str(interface.get('subnet')),
-            'ip_address': str(interface.get('ip')),
+            'subnet': str(_interface.get('subnet')),
+            'ip_address': str(_interface.get('ip')),
         }
-        if 'default_gateway' in interface:
-            data['default_gateway'] = interface.get('gateway')
+        if 'gateway' in _interface:
+            data['default_gateway'] = _interface.get('gateway')
         data['force'] = '1'
-        data['system_id'] = str(machine['system_id'])
-        data['interface_id'] = str(machine['interface_set'][0]['id'])
+        data['system_id'] = str(_machine['system_id'])
+        data['interface_id'] = str(_machine['interface_set'][0]['id'])
         return data
 
+    def _get_nic_id_by_mac(self, machine, req_mac=None):
+        data = {}
+        for nic in machine['interface_set']:
+            data[nic['mac_address']] = nic['id']
+        if req_mac:
+            if req_mac in data.keys():
+                return data[req_mac]
+            else:
+                raise Exception('NIC with mac:{} not found at '
+                                'node:{}'.format(req_mac, machine['fqdn']))
+        return data
+
+    def _disconnect_all_nic(self, machine):
+        """
+            Maas will fail, in case same config's will be to apply
+            on different interfaces. In same time - not possible to push
+            whole network schema at once. Before configuring - need to clean-up
+            everything
+        :param machine:
+        :return:
+        """
+        for nic in machine['interface_set']:
+            LOG.debug("Disconnecting interface:{}".format(nic['mac_address']))
+            try:
+                self._maas.post(
+                    u'/api/2.0/nodes/{}/interfaces/{}/'.format(
+                        machine['system_id'], nic['id']), 'disconnect')
+            except Exception as e:
+                LOG.error("Failed to disconnect interface:{} on node:{}".format(
+                    nic['mac_address'], machine['fqdn']))
+                raise Exception(str(e))
+
+    def _process_interface(self, nic_data,  machine):
+        """
+            Process exactly one interface:
+                - update interface
+                - link to network
+            These functions are self-complementary, and do not require an
+            external "process" method. Those broke old-MaasObject logic,
+            though make functions more simple in case iterable tasks.
+        """
+        nic_id = self._get_nic_id_by_mac(machine, nic_data['mac'])
+
+        # Process op=link_subnet
+        link_data = {}
+        _mode = nic_data.get('mode', 'AUTO').upper()
+        if _mode == 'STATIC':
+            link_data = {
+                'mode': 'STATIC',
+                'subnet': str(nic_data.get('subnet')),
+                'ip_address': str(nic_data.get('ip')),
+                'default_gateway': str(nic_data.get('gateway', "")),
+            }
+        elif _mode == 'DHCP':
+            link_data = {
+                'mode': 'DHCP',
+                'subnet': str(nic_data.get('subnet')),
+            }
+        elif _mode == 'AUTO':
+            link_data = {'mode': 'AUTO',
+                         'default_gateway': str(nic_data.get('gateway', "")), }
+        elif _mode in ('LINK_UP', 'UNCONFIGURED'):
+            link_data = {'mode': 'LINK_UP'}
+        else:
+            raise Exception('Wrong IP mode:{}'
+                            ' for node:{}'.format(_mode, machine['fqdn']))
+        link_data['force'] = str(1)
+
+        physical_data = {"name": nic_data.get("name", ""),
+                         "tags": nic_data.get('tags', ""),
+                         "vlan": nic_data.get('vlan', "")}
+
+        try:
+            # Cleanup-old definition
+            LOG.debug("Processing {}:{},{}".format(nic_data['mac'], link_data,
+                                                   physical_data))
+            # "link_subnet" and "fill all other data" - its 2 different
+            # operations. So, first we update NIC:
+            self._maas.put(
+                u'/api/2.0/nodes/{}/interfaces/{}/'.format(machine['system_id'],
+                                                           nic_id),
+                **physical_data)
+            # And then, link subnet configuration:
+            self._maas.post(
+                u'/api/2.0/nodes/{}/interfaces/{}/'.format(machine['system_id'],
+                                                           nic_id),
+                'link_subnet', **link_data)
+        except Exception as e:
+            LOG.error("Failed to process interface:{} on node:{}".format(
+                nic_data['mac'], machine['fqdn']))
+            raise Exception(str(e))
+
+    def fill_data(self, name, data, machines):
+        machine = machines[name]
+        if machine['status'] == self.DEPLOYED:
+            LOG.debug("Skipping node:{} "
+                      "since it in status:DEPLOYED".format(name))
+            return
+        if machine['status'] != self.READY:
+            raise Exception('Machine:{} not in status:READY'.format(name))
+        # backward comparability, for old schema
+        if data.get("interface", None):
+            if 'ip' not in data["interface"]:
+                LOG.info("No IP NIC definition for:{}".format(name))
+                return
+            LOG.warning(
+                "Old machine-describe detected! "
+                "Please read documentation "
+                "'salt-formulas/maas' for migration!")
+            return self._data_old(data['interface'], machines[name])
+        # NewSchema processing:
+        # Warning: old-style MaasObject.process still be called, but
+        # with empty data for process.
+        interfaces = data.get('interfaces', {})
+        if len(interfaces.keys()) == 0:
+            LOG.info("No IP NIC definition for:{}".format(name))
+            return
+        LOG.info('%s for %s', self.__class__.__name__.lower(),
+                 machine['fqdn'])
+        self._disconnect_all_nic(machine)
+        for key, value in sorted(interfaces.iteritems()):
+            self._process_interface(value, machine)
+
 
 class DeployMachines(MaasObject):
     # FIXME
@@ -773,6 +914,10 @@
 
 
 def process_assign_machines_ip(*args):
+    """
+    Manage interface configurations.
+    See readme.rst for more info
+    """
     return AssignMachinesIP().process(*args)