forked from crond-jaist/cylms
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlms_mgmt.py
275 lines (229 loc) · 12.1 KB
/
lms_mgmt.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
#############################################################################
# LMS management functionality for CyLMS
#############################################################################
# External imports
import subprocess
import logging
import os
import sys
# Internal imports
import cfg_mgmt
from storyboard import Storyboard
#############################################################################
# Constants
#############################################################################
## Simulation mode flag for testing purposes
SIMULATION_MODE = False
## ssh connection options (need to be separated because of limitations on how
## options are passed to subprocess.check_output() calls)
SSH_OPT1 = "-o UserKnownHostsFile=/dev/null"
SSH_OPT2 = "-o StrictHostKeyChecking=no"
## Moosh-related constants
MOOSH_COMMAND = "/root/moosh/moosh.php -p /var/www/html/moodle/"
ACTIVITY_ID_FIELD = "cmid="
### Constants below are based on values from /var/www/html/moodle/mod/scorm/locallib.php
### (and from /var/www/html/moodle/lang/en/moodle.php for showdescription)
DESCRIPTION_OPTION ="intro"
SHOW_DESCRIPTION_ENABLED="showdescription=1"
PACKAGE_FILEPATH_OPTION = "packagefilepath"
UPDATE_FREQUENCY_OPTION = "updatefreq"
SCORM_UPDATE_NEVER = 0
#SCORM_UPDATE_EVERYTIME = 3
## Test action-related constants
GET_ID_ACTION = 0
ADD_ACTION = 1
DELETE_ACTION = 2
COPY_ACTION = 3
#############################################################################
# Class that contains LMS management functionality
#############################################################################
class LmsManager:
# Constructor
def __init__(self, config_file):
# Initialize configuration manager
self.cfg_manager = cfg_mgmt.CfgManager(config_file)
# Initialize internal variables
self.lms_host = self.cfg_manager.get_setting(Storyboard.CONFIG_LMS_HOST)
self.lms_repository = self.cfg_manager.get_setting(Storyboard.CONFIG_LMS_REPOSITORY)
self.course_name = self.cfg_manager.get_setting(Storyboard.CONFIG_COURSE_NAME)
self.section_id = self.cfg_manager.get_setting(Storyboard.CONFIG_SECTION_ID)
# Display debug info
logging.debug("LMS manager settings:")
logging.debug(" - LMS host: {}".format(self.lms_host))
logging.debug(" - LMS repository: {}".format(self.lms_repository))
logging.debug(" - Course name: {}".format(self.course_name))
logging.debug(" - Section id: {}".format(self.section_id))
# Get the id of the course with given name
def get_course_id(self):
if SIMULATION_MODE:
course_id = 999
logging.debug("Simulation mode: Matching course id: {}".format(course_id))
return course_id
else:
# Get course list
ssh_output = subprocess.check_output(["ssh", SSH_OPT1, SSH_OPT2, self.lms_host, MOOSH_COMMAND,
"course-list"], stderr=subprocess.STDOUT)
logging.debug("Course list output: {}".format(ssh_output.rstrip()))
# Find the appropriate course
for output_line in ssh_output.splitlines():
# Extract the course id
if self.course_name in output_line:
logging.debug("Matching course info: {}".format(output_line.rstrip()))
# Split line of form "2","Top/CROND","CyTrONE Training","CyTrONE Training","1"
course_id = output_line.split(",")[0]
# Split id string of form "2"
course_id = course_id.split('"')[1]
logging.debug("Extracted id of matching course: {}".format(course_id))
return course_id
# If we reach this point, it means the course name was not found
logging.error("No matching record for course '{}'".format(self.course_name))
return None
# Add an activity based on course id, section id and package file
def add_activity(self, activity_name, activity_description, package_file):
if SIMULATION_MODE:
logging.debug("Simulation mode: Add activity '{}'.".format(activity_name))
activity_id = 99
return activity_id
else:
# Get the course id
course_id = self.get_course_id()
if course_id:
logging.debug("Retrieved id for course '{}': {}".format(self.course_name, course_id))
try:
# Add repository prefix to target file
package_file = self.lms_repository + package_file
options_string = DESCRIPTION_OPTION + "='" + activity_description + "'," \
+ SHOW_DESCRIPTION_ENABLED + "," \
+ PACKAGE_FILEPATH_OPTION + "=" + package_file + "," \
+ UPDATE_FREQUENCY_OPTION + "=" + str(SCORM_UPDATE_NEVER)
logging.debug("Options string: {}".format(options_string))
logging.debug("Options string: {}".format(activity_name))
# NOTE: Quoting style changed below for activity name, as the name itself may include single quotes
ssh_output = subprocess.check_output(
["ssh", SSH_OPT1, SSH_OPT2, self.lms_host, MOOSH_COMMAND, "activity-add",
"--section " + self.section_id, '--name "' + activity_name + '"',
"--options " + options_string, "scorm", course_id],
stderr=subprocess.STDOUT)
logging.debug("Add activity output: {}".format(ssh_output.rstrip()))
# Determine the activity id
for output_line in ssh_output.splitlines():
# Extract the activity id from cmid line
if ACTIVITY_ID_FIELD in output_line:
activity_id = output_line.split("=")[1]
if activity_id:
logging.info("Added activity '{}' with id '{}' for course '{}' section '{}'."
.format(activity_name, activity_id, course_id, self.section_id))
return activity_id
# If we reach this point, it means an error occurred
logging.error("Error when determining the activity id\n Command output: {}".format(ssh_output))
return None
# Any execution error will lead to an exception, which we handle below
except subprocess.CalledProcessError as error:
logging.error("Error when adding activity for course id '{}' section id '{}'\n Error message: {}"
.format(course_id, self.section_id, error.output))
return None
# If we reach this point, an error occurred
logging.error("Failed to add activity for course '{}'.".format(self.course_name))
return None
# Delete an activity with given id
def delete_activity(self, activity_id, package_file):
if SIMULATION_MODE:
logging.debug("Simulation mode: Delete activity with id '{}'.".format(activity_id))
return True
else:
# Delete activity
try:
ssh_output = subprocess.check_output(["ssh", SSH_OPT1, SSH_OPT2, self.lms_host, MOOSH_COMMAND,
"activity-delete", str(activity_id)],
stderr=subprocess.STDOUT)
logging.debug("Delete activity output: {}".format(ssh_output.rstrip()))
# If deletion succeeds, we also remove the associated package file
package_file = "-f " + self.lms_repository + package_file
subprocess.check_output(["ssh", SSH_OPT1, SSH_OPT2, self.lms_host, "rm", package_file],
stderr=subprocess.STDOUT)
# If we reach this point, it means no error occured
return True
# Any execution error will lead to an exception, which we handle below
except subprocess.CalledProcessError as error:
logging.error("Error when deleting activity with id '{}'\n Error message: {}"
.format(activity_id, error.output.rstrip()))
return False
# Copy the SCORM package to Moodle according to configuration file options
def copy_package(self, package_file, target_file):
# Add repository prefix to target file
target_file = self.lms_repository + target_file
if SIMULATION_MODE:
logging.info("Simulation mode: Copy package '{}' to\n\tTarget '{}' on {}.".format(package_file, target_file, self.lms_host))
return True
else:
# Display operation info
logging.info("Copy package '{}' to\n\tTarget '{}' on {}.".format(package_file, target_file, self.lms_host))
command = "scp -q {} {} {} {}:{}".format(SSH_OPT1, SSH_OPT2, package_file, self.lms_host, target_file)
logging.debug("Copy command: {}".format(command))
return_value = os.system(command)
exit_status = os.WEXITSTATUS(return_value)
if exit_status != 0:
logging.error("Copy package operation failed.")
return False
return True
#############################################################################
# Main program (used for testing purposes)
#############################################################################
def main():
# Configure logging level for running the tests below
logging.basicConfig(level=logging.INFO,
format='* %(levelname)s: %(filename)s: %(message)s')
# Create an LMS manager object
lms_manager = LmsManager(Storyboard.DEFAULT_CONFIG_FILE)
# Define action
action = GET_ID_ACTION
#action = ADD_ACTION
#action = DELETE_ACTION
#action = COPY_ACTION
# Perform actions
## Get course id
if action == GET_ID_ACTION:
course_id = lms_manager.get_course_id()
if course_id:
logging.info("Retrieved id for course with name '{}': {}".format(lms_manager.course_name, course_id))
else:
logging.error("Failed to get id for course '{}'.".format(lms_manager.course_name))
sys.exit(1)
## Add activity
elif action == ADD_ACTION:
activity_name = "Activity #N: Training name"
activity_description = "Created on YYYY-MM-DD hh:mm:ss"
package_file = "training_example.yml.zip"
activity_id = lms_manager.add_activity(activity_name, activity_description, package_file)
if activity_id:
logging.info("Added activity with id '{}'.".format(activity_id))
else:
logging.error("Failed to add activity '{}'.".format(activity_name))
sys.exit(1)
## Delete activity
elif action == DELETE_ACTION:
activity_id = 99
package_file = "training_example.yml.zip"
success_status = lms_manager.delete_activity(activity_id, package_file)
if success_status:
logging.info("Deleted activity with id '{}'.".format(activity_id))
else:
logging.error("Failed to delete activity with id '{}'.".format(activity_id))
sys.exit(1)
## Copy package
elif action == COPY_ACTION:
package_file = "training_example.yml.zip"
target_file = package_file
success_status = lms_manager.copy_package(package_file, target_file)
if success_status:
logging.info("Copied package '{}' to LMS repository.".format(package_file))
else:
logging.error("Failed to copy package '{}'.".format(package_file))
sys.exit(1)
else:
logging.error("No action was selected => do nothing.")
#############################################################################
# Run main program
#############################################################################
if __name__ == "__main__":
main()