~ubuntu-branches/ubuntu/lucid/landscape-client/lucid-updates

« back to all changes in this revision

Viewing changes to landscape/ui/model/configuration/state.py

  • Committer: Package Import Robot
  • Author(s): Andreas Hasenack
  • Date: 2012-04-10 14:28:48 UTC
  • mfrom: (1.1.27)
  • mto: This revision was merged to the branch mainline in revision 35.
  • Revision ID: package-import@ubuntu.com-20120410142848-7xsy4g2xii7y7ntc
ImportĀ upstreamĀ versionĀ 12.04.3

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import copy
 
2
 
 
3
from landscape.lib.network import get_fqdn
 
4
 
 
5
from landscape.ui.constants import CANONICAL_MANAGED, NOT_MANAGED
 
6
from landscape.ui.model.configuration.proxy import ConfigurationProxy
 
7
 
 
8
 
 
9
HOSTED_LANDSCAPE_HOST = "landscape.canonical.com"
 
10
LOCAL_LANDSCAPE_HOST = ""
 
11
 
 
12
HOSTED_ACCOUNT_NAME = ""
 
13
LOCAL_ACCOUNT_NAME = "standalone"
 
14
 
 
15
HOSTED_PASSWORD = ""
 
16
LOCAL_PASSWORD = ""
 
17
 
 
18
HOSTED = "hosted"
 
19
LOCAL = "local"
 
20
MANAGEMENT_TYPE = "management-type"
 
21
COMPUTER_TITLE = "computer-title"
 
22
LANDSCAPE_HOST = "landscape-host"
 
23
ACCOUNT_NAME = "account-name"
 
24
PASSWORD = "password"
 
25
 
 
26
 
 
27
DEFAULT_DATA = {
 
28
    MANAGEMENT_TYPE: NOT_MANAGED,
 
29
    COMPUTER_TITLE: get_fqdn(),
 
30
    HOSTED: {
 
31
        LANDSCAPE_HOST: HOSTED_LANDSCAPE_HOST,
 
32
        ACCOUNT_NAME: HOSTED_ACCOUNT_NAME,
 
33
        PASSWORD: HOSTED_PASSWORD},
 
34
    LOCAL: {
 
35
        LANDSCAPE_HOST: LOCAL_LANDSCAPE_HOST,
 
36
        ACCOUNT_NAME: LOCAL_ACCOUNT_NAME,
 
37
        PASSWORD: LOCAL_PASSWORD}}
 
38
 
 
39
 
 
40
def derive_server_host_name_from_url(url):
 
41
    """
 
42
    Extract the hostname part from a URL.
 
43
    """
 
44
    try:
 
45
        without_protocol = url[url.index("://") + 3:]
 
46
    except ValueError:
 
47
        without_protocol = url
 
48
    try:
 
49
        return without_protocol[:without_protocol.index("/")]
 
50
    except ValueError:
 
51
        return without_protocol
 
52
 
 
53
 
 
54
def derive_url_from_host_name(host_name):
 
55
    """
 
56
    Extrapolate a url from a host name.
 
57
    """
 
58
    #Reuse this code to make sure it's a proper host name
 
59
    host_name = derive_server_host_name_from_url(host_name)
 
60
    return "https://" + host_name + "/message-system"
 
61
 
 
62
 
 
63
def derive_ping_url_from_host_name(host_name):
 
64
    """
 
65
    Extrapolate a ping_url from a host name.
 
66
    """
 
67
    #Reuse this code to make sure it's a proper host name
 
68
    host_name = derive_server_host_name_from_url(host_name)
 
69
    return "http://" + host_name + "/ping"
 
70
 
 
71
 
 
72
class StateError(Exception):
 
73
    """
 
74
    An exception that is raised when there is an error relating to the current
 
75
    state.
 
76
    """
 
77
 
 
78
 
 
79
class TransitionError(Exception):
 
80
    """
 
81
    An L{Exception} that is raised when a valid transition between states fails
 
82
    for some non state related reason.  For example, this error is raised when
 
83
    the user does not have the privilege of reading the configuration file,
 
84
    this causes the transition from L{VirginState} to L{InitialisedState} to
 
85
    fail but not because that transition from one state to another was not
 
86
    permitted, but rather the transition encountered an error.
 
87
    """
 
88
 
 
89
 
 
90
class ConfigurationState(object):
 
91
    """
 
92
    Base class for states used in the L{ConfigurationModel}.
 
93
    """
 
94
    def __init__(self, data, proxy, uisettings):
 
95
        self._data = copy.deepcopy(data)
 
96
        self._proxy = proxy
 
97
        self._uisettings = uisettings
 
98
 
 
99
    def get_config_filename(self):
 
100
        return self._proxy.get_config_filename()
 
101
 
 
102
    def get(self, *args):
 
103
        """
 
104
        Retrieve only valid values from two level dictionary based tree.
 
105
 
 
106
        This mainly served to pick up programming errors and could easily be
 
107
        replaced with a simpler scheme.
 
108
        """
 
109
        arglen = len(args)
 
110
        if arglen > 2 or arglen == 0:
 
111
            raise TypeError(
 
112
                "get() takes either 1 or 2 keys (%d given)" % arglen)
 
113
        if arglen == 2:  # We're looking for a leaf on a branch
 
114
            sub_dict = None
 
115
            if args[0] in [HOSTED, LOCAL]:
 
116
                sub_dict = self._data.get(args[0], {})
 
117
            sub_dict = self._data[args[0]]
 
118
            if not isinstance(sub_dict, dict):
 
119
                raise KeyError(
 
120
                    "Compound key [%s][%s] is invalid. The data type " +
 
121
                    "returned from the first index was %s." %
 
122
                    sub_dict.__class__.__name__)
 
123
            return sub_dict.get(args[1], None)
 
124
        else:
 
125
            if args[0] in (MANAGEMENT_TYPE, COMPUTER_TITLE):
 
126
                return self._data.get(args[0], None)
 
127
            else:
 
128
                raise KeyError("Key [%s] is invalid. " % args[0])
 
129
 
 
130
    def set(self, *args):
 
131
        """
 
132
        Set only valid values from two level dictionary based tree.
 
133
 
 
134
        This mainly served to pick up programming errors and could easily be
 
135
        replaced with a simpler scheme.
 
136
        """
 
137
        arglen = len(args)
 
138
        if arglen < 2 or arglen > 3:
 
139
            raise TypeError("set() takes either 1 or 2 keys and exactly 1 " +
 
140
                            "value (%d arguments given)" % arglen)
 
141
        if arglen == 2:  # We're setting a leaf attached to the root
 
142
            self._data[args[0]] = args[1]
 
143
        else:  # We're setting a leaf on a branch
 
144
            sub_dict = None
 
145
            if args[0] in [HOSTED, LOCAL]:
 
146
                sub_dict = self._data.get(args[0], {})
 
147
            if not isinstance(sub_dict, dict):
 
148
                raise KeyError("Compound key [%s][%s] is invalid. The data " +
 
149
                               "type returned from the first index was %s."
 
150
                               % sub_dict.__class__.__name__)
 
151
            sub_dict[args[1]] = args[2]
 
152
            self._data[args[0]] = sub_dict
 
153
 
 
154
    def load_data(self, asynchronous=True, exit_method=None):
 
155
        raise NotImplementedError
 
156
 
 
157
    def modify(self):
 
158
        raise NotImplementedError
 
159
 
 
160
    def revert(self):
 
161
        raise NotImplementedError
 
162
 
 
163
    def persist(self):
 
164
        raise NotImplementedError
 
165
 
 
166
    def exit(self, asynchronous=True, exit_method=None):
 
167
        return ExitedState(self._data, self._proxy, self._uisettings,
 
168
                           asynchronous=asynchronous, exit_method=exit_method)
 
169
 
 
170
 
 
171
class Helper(object):
 
172
    """
 
173
    Base class for all state transition helpers.
 
174
 
 
175
    It is assumed that the Helper classes are "friends" of the
 
176
    L{ConfigurationState} classes and can have some knowledge of their
 
177
    internals.  They shouldn't be visible to users of the
 
178
    L{ConfigurationState}s and in general we should avoid seeing the
 
179
    L{ConfigurationState}'s _data attribute outside this module.
 
180
    """
 
181
 
 
182
    def __init__(self, state):
 
183
        self._state = state
 
184
 
 
185
 
 
186
class ModifiableHelper(Helper):
 
187
    """
 
188
    Allow a L{ConfigurationState}s to be modified.
 
189
    """
 
190
 
 
191
    def modify(self):
 
192
        return ModifiedState(self._state._data, self._state._proxy,
 
193
                             self._state._uisettings)
 
194
 
 
195
 
 
196
class UnloadableHelper(Helper):
 
197
    """
 
198
    Disallow loading of data into a L{ConfigurationModel}.
 
199
    """
 
200
 
 
201
    def load_data(self, asynchronous=True, exit_method=None):
 
202
        raise StateError("A ConfiguratiomModel in a " +
 
203
                         self.__class__.__name__ +
 
204
                         " cannot be transitioned via load_data()")
 
205
 
 
206
 
 
207
class UnmodifiableHelper(Helper):
 
208
    """
 
209
    Disallow modification of a L{ConfigurationState}.
 
210
    """
 
211
 
 
212
    def modify(self):
 
213
        raise StateError("A ConfigurationModel in " +
 
214
                         self.__class__.__name__ +
 
215
                         " cannot transition via modify()")
 
216
 
 
217
 
 
218
class RevertableHelper(Helper):
 
219
    """
 
220
    Allow reverting of a L{ConfigurationModel}.
 
221
    """
 
222
 
 
223
    def revert(self):
 
224
        return InitialisedState(self._state._data, self._state._proxy,
 
225
                                self._state._uisettings)
 
226
 
 
227
 
 
228
class UnrevertableHelper(Helper):
 
229
    """
 
230
    Disallow reverting of a L{ConfigurationModel}.
 
231
    """
 
232
 
 
233
    def revert(self):
 
234
        raise StateError("A ConfigurationModel in " +
 
235
                         self.__class__.__name__ +
 
236
                         " cannot transition via revert()")
 
237
 
 
238
 
 
239
class PersistableHelper(Helper):
 
240
    """
 
241
    Allow a L{ConfigurationModel} to persist.
 
242
    """
 
243
 
 
244
    def _save_to_uisettings(self):
 
245
        """
 
246
        Persist full content to the L{UISettings} object.
 
247
        """
 
248
        self._state._uisettings.set_management_type(
 
249
            self._state.get(MANAGEMENT_TYPE))
 
250
        self._state._uisettings.set_computer_title(
 
251
            self._state.get(COMPUTER_TITLE))
 
252
        self._state._uisettings.set_hosted_account_name(
 
253
            self._state.get(HOSTED, ACCOUNT_NAME))
 
254
        self._state._uisettings.set_hosted_password(
 
255
            self._state.get(HOSTED, PASSWORD))
 
256
        self._state._uisettings.set_local_landscape_host(
 
257
            self._state.get(LOCAL, LANDSCAPE_HOST))
 
258
        self._state._uisettings.set_local_account_name(
 
259
            self._state.get(LOCAL, ACCOUNT_NAME))
 
260
        self._state._uisettings.set_local_password(
 
261
            self._state.get(LOCAL, PASSWORD))
 
262
 
 
263
    def _save_to_config(self):
 
264
        """
 
265
        Persist the subset of the data we want to make live to the actual
 
266
        configuration file.
 
267
        """
 
268
        hosted = self._state.get(MANAGEMENT_TYPE)
 
269
        if hosted is NOT_MANAGED:
 
270
            pass
 
271
        else:
 
272
            if hosted == CANONICAL_MANAGED:
 
273
                first_key = HOSTED
 
274
            else:
 
275
                first_key = LOCAL
 
276
            self._state._proxy.url = derive_url_from_host_name(
 
277
                self._state.get(first_key, LANDSCAPE_HOST))
 
278
            self._state._proxy.ping_url = derive_ping_url_from_host_name(
 
279
                self._state.get(first_key, LANDSCAPE_HOST))
 
280
            self._state._proxy.account_name = self._state.get(
 
281
                first_key, ACCOUNT_NAME)
 
282
            self._state._proxy.registration_password = self._state.get(
 
283
                first_key, PASSWORD)
 
284
            self._state._proxy.computer_title = self._state.get(COMPUTER_TITLE)
 
285
            self._state._proxy.write()
 
286
 
 
287
    def persist(self):
 
288
        self._save_to_uisettings()
 
289
        self._save_to_config()
 
290
        return InitialisedState(self._state._data, self._state._proxy,
 
291
                                self._state._uisettings)
 
292
 
 
293
 
 
294
class UnpersistableHelper(Helper):
 
295
    """
 
296
    Disallow persistence of a L{ConfigurationModel}.
 
297
    """
 
298
 
 
299
    def persist(self):
 
300
        raise StateError("A ConfiguratonModel in " +
 
301
                         self.__class__.__name__ +
 
302
                         " cannot be transitioned via persist().")
 
303
 
 
304
 
 
305
class ExitedState(ConfigurationState):
 
306
    """
 
307
    The terminal state of L{ConfigurationModel}, you can't do anything further
 
308
    once this state is reached.
 
309
    """
 
310
    def __init__(self, data, proxy, uisettings, exit_method=None,
 
311
                 asynchronous=True):
 
312
        super(ExitedState, self).__init__(None, None, None)
 
313
        if callable(exit_method):
 
314
            exit_method()
 
315
        else:
 
316
            proxy.exit(asynchronous=asynchronous)
 
317
        self._unloadable_helper = UnloadableHelper(self)
 
318
        self._unmodifiable_helper = UnmodifiableHelper(self)
 
319
        self._unrevertable_helper = UnrevertableHelper(self)
 
320
        self._unpersistable_helper = UnpersistableHelper(self)
 
321
 
 
322
    def load_data(self, asynchronous=True, exit_method=None):
 
323
        return self._unloadable_helper.load_data(asynchronous=asynchronous,
 
324
                                                 exit_method=exit_method)
 
325
 
 
326
    def modify(self):
 
327
        return self._unmodifiable_helper.modify()
 
328
 
 
329
    def revert(self):
 
330
        return self._unrevertable_helper.revert()
 
331
 
 
332
    def persist(self):
 
333
        return self._unpersistable_helper.persist()
 
334
 
 
335
    def exit(self, asynchronous=True):
 
336
        return self
 
337
 
 
338
 
 
339
class ModifiedState(ConfigurationState):
 
340
    """
 
341
    The state of a L{ConfigurationModel} whenever the user has modified some
 
342
    data but hasn't yet L{persist}ed or L{revert}ed.
 
343
    """
 
344
 
 
345
    def __init__(self, data, proxy, uisettings):
 
346
        super(ModifiedState, self).__init__(data, proxy, uisettings)
 
347
        self._modifiable_helper = ModifiableHelper(self)
 
348
        self._revertable_helper = RevertableHelper(self)
 
349
        self._persistable_helper = PersistableHelper(self)
 
350
 
 
351
    def modify(self):
 
352
        return self._modifiable_helper.modify()
 
353
 
 
354
    def revert(self):
 
355
        return self._revertable_helper.revert()
 
356
 
 
357
    def persist(self):
 
358
        return self._persistable_helper.persist()
 
359
 
 
360
 
 
361
class InitialisedState(ConfigurationState):
 
362
    """
 
363
    The state of the L{ConfigurationModel} as initially presented to the
 
364
    user. Baseline data should have been loaded from the real configuration
 
365
    data, any persisted user data should be loaded into blank values and
 
366
    finally defaults should be applied where necessary.
 
367
    """
 
368
 
 
369
    def __init__(self, data, proxy, uisettings):
 
370
        super(InitialisedState, self).__init__(data, proxy, uisettings)
 
371
        self._modifiable_helper = ModifiableHelper(self)
 
372
        self._unrevertable_helper = UnrevertableHelper(self)
 
373
        self._unpersistable_helper = UnpersistableHelper(self)
 
374
        self._load_uisettings_data()
 
375
        if not self._load_live_data():
 
376
            raise TransitionError("Authentication Failure")
 
377
 
 
378
    def _load_uisettings_data(self):
 
379
        """
 
380
        Load the complete set of dialog data from L{UISettings}.
 
381
        """
 
382
        hosted = self._uisettings.get_management_type()
 
383
        self.set(MANAGEMENT_TYPE, hosted)
 
384
        computer_title = self._uisettings.get_computer_title()
 
385
        if computer_title:
 
386
            self.set(COMPUTER_TITLE, computer_title)
 
387
        self.set(HOSTED, ACCOUNT_NAME,
 
388
                 self._uisettings.get_hosted_account_name())
 
389
        self.set(HOSTED, PASSWORD, self._uisettings.get_hosted_password())
 
390
        self.set(LOCAL, LANDSCAPE_HOST,
 
391
                 self._uisettings.get_local_landscape_host())
 
392
        local_account_name = self._uisettings.get_local_account_name()
 
393
        if local_account_name:
 
394
            self.set(LOCAL, ACCOUNT_NAME, local_account_name)
 
395
        self.set(LOCAL, PASSWORD, self._uisettings.get_local_password())
 
396
 
 
397
    def _load_live_data(self):
 
398
        """
 
399
        Load the current live subset of data from the configuration file.
 
400
        """
 
401
        if self._proxy.load(None):
 
402
            computer_title = self._proxy.computer_title
 
403
            if computer_title:
 
404
                self.set(COMPUTER_TITLE, computer_title)
 
405
            url = self._proxy.url
 
406
            if url.find(HOSTED_LANDSCAPE_HOST) > -1:
 
407
                self.set(HOSTED, ACCOUNT_NAME, self._proxy.account_name)
 
408
                self.set(HOSTED, PASSWORD, self._proxy.registration_password)
 
409
            else:
 
410
                self.set(LOCAL, LANDSCAPE_HOST,
 
411
                         derive_server_host_name_from_url(url))
 
412
                if self._proxy.account_name != "":
 
413
                    self.set(LOCAL, ACCOUNT_NAME, self._proxy.account_name)
 
414
            return True
 
415
        else:
 
416
            return False
 
417
 
 
418
    def load_data(self, asynchronous=True, exit_method=None):
 
419
        return self
 
420
 
 
421
    def modify(self):
 
422
        return self._modifiable_helper.modify()
 
423
 
 
424
    def revert(self):
 
425
        return self._unrevertable_helper.revert()
 
426
 
 
427
    def persist(self):
 
428
        return self._unpersistable_helper.persist()
 
429
 
 
430
 
 
431
class VirginState(ConfigurationState):
 
432
    """
 
433
    The state of the L{ConfigurationModel} before any actions have been taken
 
434
    upon it.
 
435
    """
 
436
 
 
437
    def __init__(self, proxy, uisettings):
 
438
        super(VirginState, self).__init__(DEFAULT_DATA, proxy, uisettings)
 
439
        self._unmodifiable_helper = UnmodifiableHelper(self)
 
440
        self._unrevertable_helper = UnrevertableHelper(self)
 
441
        self._unpersistable_helper = UnpersistableHelper(self)
 
442
 
 
443
    def load_data(self, asynchronous=True, exit_method=None):
 
444
        try:
 
445
            return InitialisedState(self._data, self._proxy, self._uisettings)
 
446
        except TransitionError:
 
447
            return ExitedState(self._data, self._proxy, self._uisettings,
 
448
                               asynchronous=asynchronous,
 
449
                               exit_method=exit_method)
 
450
 
 
451
    def modify(self):
 
452
        return self._unmodifiable_helper.modify()
 
453
 
 
454
    def revert(self):
 
455
        return self._unrevertable_helper.revert()
 
456
 
 
457
    def persist(self):
 
458
        return self._unpersistable_helper.persist()
 
459
 
 
460
 
 
461
class ConfigurationModel(object):
 
462
    """
 
463
    L{ConfigurationModel} presents a model of configuration as the UI
 
464
    requirements describe it (separate values for the Hosted and Local
 
465
    configurations) as opposed to the real structure of the configuration
 
466
    file.  This is intended to achieve the following:
 
467
 
 
468
       1. Allow the expected behaviour in the UI without changing the live
 
469
          config file.
 
470
       2. Supersede the overly complex logic in the controller layer with a
 
471
          cleaner state pattern.
 
472
 
 
473
    The allowable state transitions are:
 
474
 
 
475
       VirginState      --(load_data)--> InitialisedState
 
476
       VirginState      --(load_data)--> ExitedState
 
477
       VirginState      --(exit)-------> ExitedState
 
478
       InitialisedState --(modify)-----> ModifiedState
 
479
       InitialisedState --(exit)-------> ExitedState
 
480
       ModifiedState    --(revert)-----> InitialisedState
 
481
       ModifiedState    --(modify)-----> ModifiedState
 
482
       ModifiedState    --(persist)----> InitialisedState
 
483
       ModifiedState    --(exit)-------> ExitedState
 
484
    """
 
485
 
 
486
    def __init__(self, proxy=None, proxy_loadargs=[], uisettings=None):
 
487
        if not proxy:
 
488
            proxy = ConfigurationProxy(loadargs=proxy_loadargs)
 
489
        self._current_state = VirginState(proxy, uisettings)
 
490
 
 
491
    def get_state(self):
 
492
        """
 
493
        Expose the underlying L{ConfigurationState}, for testing purposes.
 
494
        """
 
495
        return self._current_state
 
496
 
 
497
    def load_data(self, asynchronous=True, exit_method=None):
 
498
        self._current_state = self._current_state.load_data(
 
499
            asynchronous=asynchronous, exit_method=exit_method)
 
500
        return isinstance(self._current_state, InitialisedState)
 
501
 
 
502
    def modify(self):
 
503
        self._current_state = self._current_state.modify()
 
504
 
 
505
    def revert(self):
 
506
        self._current_state = self._current_state.revert()
 
507
 
 
508
    def persist(self):
 
509
        self._current_state = self._current_state.persist()
 
510
 
 
511
    def _get_management_type(self):
 
512
        return self._current_state.get(MANAGEMENT_TYPE)
 
513
 
 
514
    def _set_management_type(self, value):
 
515
        self._current_state.set(MANAGEMENT_TYPE, value)
 
516
 
 
517
    management_type = property(_get_management_type, _set_management_type)
 
518
 
 
519
    def _get_computer_title(self):
 
520
        return self._current_state.get(COMPUTER_TITLE)
 
521
 
 
522
    def _set_computer_title(self, value):
 
523
        self._current_state.set(COMPUTER_TITLE, value)
 
524
 
 
525
    computer_title = property(_get_computer_title, _set_computer_title)
 
526
 
 
527
    def _get_hosted_landscape_host(self):
 
528
        return self._current_state.get(HOSTED, LANDSCAPE_HOST)
 
529
 
 
530
    hosted_landscape_host = property(_get_hosted_landscape_host)
 
531
 
 
532
    def _get_local_landscape_host(self):
 
533
        return self._current_state.get(LOCAL, LANDSCAPE_HOST)
 
534
 
 
535
    def _set_local_landscape_host(self, value):
 
536
        self._current_state.set(LOCAL, LANDSCAPE_HOST, value)
 
537
 
 
538
    local_landscape_host = property(_get_local_landscape_host,
 
539
                                    _set_local_landscape_host)
 
540
 
 
541
    def _get_hosted_account_name(self):
 
542
        return self._current_state.get(HOSTED, ACCOUNT_NAME)
 
543
 
 
544
    def _set_hosted_account_name(self, value):
 
545
        self._current_state.set(HOSTED, ACCOUNT_NAME, value)
 
546
 
 
547
    hosted_account_name = property(_get_hosted_account_name,
 
548
                                   _set_hosted_account_name)
 
549
 
 
550
    def _get_local_account_name(self):
 
551
        return self._current_state.get(LOCAL, ACCOUNT_NAME)
 
552
 
 
553
    def _set_local_account_name(self, value):
 
554
        self._current_state.set(LOCAL, ACCOUNT_NAME, value)
 
555
 
 
556
    local_account_name = property(_get_local_account_name,
 
557
                                   _set_local_account_name)
 
558
 
 
559
    def _get_hosted_password(self):
 
560
        return self._current_state.get(HOSTED, PASSWORD)
 
561
 
 
562
    def _set_hosted_password(self, value):
 
563
        self._current_state.set(HOSTED, PASSWORD, value)
 
564
 
 
565
    hosted_password = property(_get_hosted_password,
 
566
                               _set_hosted_password)
 
567
 
 
568
    def _get_local_password(self):
 
569
        return self._current_state.get(LOCAL, PASSWORD)
 
570
 
 
571
    def _set_local_password(self, value):
 
572
        self._current_state.set(LOCAL, PASSWORD, value)
 
573
 
 
574
    local_password = property(_get_local_password,
 
575
                              _set_local_password)
 
576
 
 
577
    def _get_is_modified(self):
 
578
        return isinstance(self.get_state(), ModifiedState)
 
579
 
 
580
    is_modified = property(_get_is_modified)
 
581
 
 
582
    def get_config_filename(self):
 
583
        return self._current_state.get_config_filename()
 
584
 
 
585
    def exit(self, asynchronous=True):
 
586
        self._current_state.exit(asynchronous=asynchronous)