Merge lp:~javier.collado/utah/sshmixin_destroy_fixes into lp:utah

Proposed by Javier Collado
Status: Merged
Merged at revision: 491
Proposed branch: lp:~javier.collado/utah/sshmixin_destroy_fixes
Merge into: lp:utah
Diff against target: 58 lines (+15/-7)
3 files modified
utah/config.py (+2/-0)
utah/provisioning/provisioning.py (+10/-7)
utah/provisioning/vm/vm.py (+3/-0)
To merge this branch: bzr merge lp:~javier.collado/utah/sshmixin_destroy_fixes
Reviewer Review Type Date Requested Status
Max Brustkern (community) Approve
Review via email: mp+115959@code.launchpad.net

Description of the change

Fixes to make SSHMixin.destroy work.

When running some tests I got this error message:
Failed to destroy machine: Method destroy not defined for machine type CustomVM

This is a misleading error because CustomVM has a destroy method already. However, since it calls `super` there's a whole chain to take into account. According to the MRO this would be as follows:
(utah.provisioning.vm.libvirtvm.CustomVM,
 utah.provisioning.provisioning.CustomInstallMixin,
 utah.provisioning.provisioning.SSHMixin,
 utah.provisioning.vm.libvirtvm.LibvirtVM,
 utah.provisioning.vm.vm.VM,
 utah.provisioning.provisioning.Machine,
 object)

The problem happened in SSHMixin when calling `super` and that when through `LibvirtVM`, `VM` and, finally, `Machine` which is the method that raised the error. I already fixed it with an empty implementation in `VM`, but I still don't like this solution, so I'd like to further discuss this to know what do you think about this.

In my opinion, here there are two problems:
- The error is misleading because it doesn't really points out what's the class that is causing the problem
- Mixins shouldn't be involved in all this chained `super` calls.

My proposal would be to change the inheritance order. For example from this:
class CustomVM(CustomInstallMixin, SSHMixin, LibvirtVM)

to this:
class CustomVM(LibvirtVM, CustomInstallMixin, SSHMixin)

to keep MRO use classes in the hierarchy tree (hopefully single inheritance is still used and there's always a final class that doesn't call `super` to avoid calling the mixin methods).

Also if some mixin code has to be called, then it should be explicitly called like:
super(LibvirtVM, self).destroy(self) # Destroy calling parent class methods
CustomInstallMixin.destroy(self) # Destroy explicitly calling mixin methods
SSHMixin.destroy(self)

To post a comment you must log in.
Revision history for this message
Max Brustkern (nuclearbob) wrote :

When I originally implemented the exception-raising destroy in Machine, I wasn't thinking that destroy would be something we'd need much inheritance on. I think moving that to an empty implementation in Machine (or one that just returns False) might be a good way to go.

As far as the exceptions go, I didn't really take inheritance into account well when setting up all those methods. Perhaps a clearer exception would be something like "{class} has called the {method} method of the base Machine class, which is not implemented."

I think changing the inheritance order is going to cause problems with all those exception-creating base methods. We can remove those, but if we do that, I'd like to see some way of reporting that we've called a method that a Machine does not implement that's more readable than the default python exception. Maybe we just need to be catching that and raising something else.

Finally, a note on the config file. Anything that uses a directory or information on the system on which the package is installed currently needs to go through a special routine to prevent the config info from being written when the package is built. It's this function that does that:
def dumpdefaults(build=False):
    if build:
        del DEFAULTS['bridge']
        del DEFAULTS['logfile']
        del DEFAULTS['sshprivatekey']
        del DEFAULTS['sshpublickey']
        del DEFAULTS['user']
        del DEFAULTS['vmpath']
    return json.dumps(DEFAULTS, sort_keys=True, indent=4)
The reason I do that is so that we can ship a config file that's based on config.py, but still goes in /etc/utah and takes advantage of the debian packaging facilities for tracking user changes to config files. This may not be the best approach in the long run. If you've got ideas on that, we can discuss them. For now I'm going to make the system-specific options a separate dict which gets added the defaults, and then we can remove it if we need to.

review: Approve
Revision history for this message
Javier Collado (javier.collado) wrote :

Thanks for the explanation. I see that the output of running utah config is precisely what is stored in /etc/utah/config. The separation of data that can be hardcoded and data that is programmatically generated (mainly through os.path.expanduser) looks fine to me. Anyway, I still don't see what kind of user changes to config files are being tracked.

Regarding inheritance and the implementation of methods in different classes, maybe it should be clarified in some way what are the expectations for each class (being a mixin or not), since it's difficult to know when a class should really implement a method or not.

Revision history for this message
Max Brustkern (nuclearbob) wrote :

The change tracking for the config file is all handled by the package system. At some point we may need to move away from that for flexibility, but for now I think it's useful.

I think clarifying the expectations for class implementation of methods is a good idea. The current model was developed before I started making mixin classes, and I think that's confused things somewhat. If you have any proposals or you want to just submit some changes to the documentation, go for it.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'utah/config.py'
2--- utah/config.py 2012-07-19 20:00:02 +0000
3+++ utah/config.py 2012-07-20 11:37:44 +0000
4@@ -39,6 +39,7 @@
5 series='precise',
6 sshprivatekey=os.path.join(os.path.expanduser('~'), '.ssh', 'utah'),
7 sshpublickey=os.path.join(os.path.expanduser('~'), '.ssh', 'utah.pub'),
8+ sshknownhosts=os.path.join(os.path.expanduser('~'), '.ssh', 'known_hosts'),
9 template=None,
10 user=getpass.getuser(),
11 vmpath=os.path.join(os.path.expanduser('~'), 'vm'),
12@@ -64,6 +65,7 @@
13 del DEFAULTS['logfile']
14 del DEFAULTS['sshprivatekey']
15 del DEFAULTS['sshpublickey']
16+ del DEFAULTS['sshknownhosts']
17 del DEFAULTS['user']
18 del DEFAULTS['vmpath']
19 return json.dumps(DEFAULTS, sort_keys=True, indent=4)
20
21=== modified file 'utah/provisioning/provisioning.py'
22--- utah/provisioning/provisioning.py 2012-07-19 22:04:49 +0000
23+++ utah/provisioning/provisioning.py 2012-07-20 11:37:44 +0000
24@@ -426,13 +426,16 @@
25 else:
26 raise err
27
28- # host_keys doesn't support __delitem__, so pop doesn't work either.
29- # TODO: fix this?
30- #host_keys = self.ssh_client.get_host_keys()
31- #for address in addresses:
32- # self.logger.debug('Removing ' + address + ' from known_hosts')
33- # del host_keys[address]
34- #self.ssh_client.save_host_keys()
35+ old_host_keys = self.ssh_client.get_host_keys()
36+ new_host_keys = paramiko.HostKeys()
37+ addresses = set(addresses)
38+ for address, key in old_host_keys.iteritems():
39+ # Skip keys so that they don't get added
40+ # into the new keys (i.e. they're removed)
41+ if address in addresses:
42+ continue
43+ new_host_keys[address] = key
44+ new_host_keys.save(config.sshknownhosts)
45 self.ssh_client.close()
46
47 super(SSHMixin, self).destroy(*args, **kw)
48
49=== modified file 'utah/provisioning/vm/vm.py'
50--- utah/provisioning/vm/vm.py 2012-07-18 11:07:33 +0000
51+++ utah/provisioning/vm/vm.py 2012-07-20 11:37:44 +0000
52@@ -12,3 +12,6 @@
53 def __init__(self, *args, **kw):
54 super(VM, self).__init__(*args, **kw)
55 self.logger.debug('VM init finished')
56+
57+ def destroy(self, *args, **kwargs):
58+ pass

Subscribers

People subscribed via source and target branches