├── .gitignore ├── FOSDEM2024 ├── 3D_print │ ├── cmkhexagon.scad │ ├── cmkhexagon.stl │ ├── cmkhexagonbase.scad │ ├── cmkhexagonbase.stl │ ├── cmkhexagondrill.scad │ ├── cmkhexagondrill.stl │ ├── cmkhexagonspacer.scad │ ├── cmkhexagonspacer.stl │ ├── cmkhexpcbmount.scad │ └── cmkhexpcbmount.stl ├── Arduino │ ├── CO2-Ampel │ │ ├── examples │ │ │ ├── BMP280_Test │ │ │ │ └── BMP208_Test.ino │ │ │ ├── Blink │ │ │ │ └── Blink.ino │ │ │ ├── Button-Switch │ │ │ │ └── Button-Switch.ino │ │ │ ├── CO2-Ampel │ │ │ │ ├── CO2-Ampel.ino │ │ │ │ ├── CO2-Ampel.ino.co2ampel.bin │ │ │ │ └── CO2-Ampel.ino.with_bootloader.co2ampel.bin │ │ │ ├── ECC608_Test │ │ │ │ └── ECC608_Test.ino │ │ │ ├── ECCX08_CSR │ │ │ │ └── ECCX08_CSR.ino │ │ │ ├── I2C-Scanner │ │ │ │ └── I2C-Scanner.ino │ │ │ ├── RFM9X_TTN │ │ │ │ └── RFM9X_TTN.ino │ │ │ ├── RFM9X_Test │ │ │ │ └── RFM9X_Test.ino │ │ │ ├── SSL_Test │ │ │ │ └── SSL_Test.ino │ │ │ ├── WINC1500_Test │ │ │ │ └── WINC1500_Test.ino │ │ │ └── WINC1500_Updater │ │ │ │ ├── Endianess.ino │ │ │ │ └── WINC1500_Updater.ino │ │ ├── library.properties │ │ └── src │ │ │ └── co2ampel.h │ ├── LoraReceiverTest │ │ └── LoraReceiverTest.ino │ └── LoraSenderTest │ │ ├── LoraSenderTest.ino │ │ ├── LoraSenderTest.ino.co2ampel.bin │ │ └── LoraSenderTest.ino.with_bootloader.co2ampel.bin ├── MicroPython │ ├── discobaby.py │ └── fulltest.py ├── Python │ ├── dumplora.py │ └── status2ws.py ├── README.md └── images │ ├── IMG_20240615_150747.jpg │ ├── IMG_20240615_150804.jpg │ └── IMG_20240615_150829.jpg ├── README.md ├── bootstrap ├── README.md ├── dummybridge.sh └── quickdebuntu.sh ├── docserve ├── README.md ├── autoreload.js ├── auxiliary │ └── DocserveAuxiliary.rb ├── branding.dic ├── builddocs.rb ├── checkmk-docserve.cfg.example ├── docserve.css ├── docserve.rb ├── favicon.ico ├── favicon.png └── fold.js ├── mkp ├── hellobakery │ ├── README.md │ └── mkp │ │ ├── _agent_based │ │ └── hello_bakery.py │ │ ├── _agents │ │ ├── plugins │ │ │ └── hello_bakery │ │ └── windows │ │ │ └── plugins │ │ │ └── hello_bakery.cmd │ │ ├── _checkman │ │ └── hello_bakery │ │ ├── _lib │ │ └── check_mk │ │ │ └── base │ │ │ └── cee │ │ │ └── plugins │ │ │ └── bakery │ │ │ └── hello_bakery.py │ │ ├── _web │ │ └── plugins │ │ │ ├── metrics │ │ │ └── hellobakery_metric.py │ │ │ ├── perfometer │ │ │ └── hellobakery_perfometer.py │ │ │ └── wato │ │ │ ├── hellobakery_bakery.py │ │ │ └── hellobakery_parameters.py │ │ ├── hello_bakery-0.1.3.mkp │ │ ├── hello_bakery-0.1.6.mkp │ │ ├── hello_bakery-0.1.8.mkp │ │ ├── info │ │ └── info.json ├── helloco2 │ ├── ao2ampel.py │ └── json_receiver.py ├── helloworld │ ├── README.md │ └── mkp │ │ ├── _agent_based │ │ └── hello_world.py │ │ ├── _agents │ │ └── plugins │ │ │ └── hello_world │ │ ├── _checkman │ │ └── hello_world │ │ ├── _web │ │ └── plugins │ │ │ ├── metrics │ │ │ └── helloworld_metric.py │ │ │ ├── perfometer │ │ │ └── helloworld_perfometer.py │ │ │ └── wato │ │ │ └── helloworld_parameters.py │ │ ├── hello_world-0.1.3.mkp │ │ ├── info │ │ └── info.json └── watterott_co2_ampel │ ├── mkp │ ├── _agent_based │ │ └── co2welectronic.py │ ├── _web │ │ └── plugins │ │ │ └── wato │ │ │ └── co2welectronic_parameters.py │ ├── info │ └── info.json │ └── serial2spool.py └── scripts ├── certcompare.rb ├── fonttest.html ├── megax.sh ├── rki_covid.py ├── rki_covid_agbased.py └── rki_covid_metrics.py /.gitignore: -------------------------------------------------------------------------------- 1 | *secrets.h 2 | 3 | -------------------------------------------------------------------------------- /FOSDEM2024/3D_print/cmkhexagon.scad: -------------------------------------------------------------------------------- 1 | 2 | 3 | $fn = 120; 4 | boreheight = 8.5; 5 | 6 | difference() { 7 | union() { 8 | translate([0,0,2]) cylinder(r=50, h=10, $fn=6); 9 | cylinder(2, 48.5, 50, $fn=6); 10 | } 11 | union() { 12 | translate([0,0,2]) cylinder(r=45, h=24, $fn=6); 13 | translate([0,0,2]) cylinder(r=40, h=24); 14 | /* 15 | translate([0,0,boreheight]) rotate(a=[-90,0,0]) 16 | cylinder(150, 1.75, 1.75, center=true); 17 | translate([0,0,boreheight]) rotate(a=[-90,0,60]) 18 | cylinder(150, 1.75, 1.75, center=true); 19 | translate([0,0,boreheight]) rotate(a=[-90,0,-60]) 20 | cylinder(150, 1.75, 1.75, center=true); */ 21 | 22 | } 23 | } -------------------------------------------------------------------------------- /FOSDEM2024/3D_print/cmkhexagon.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschlenker/checkmk-snippets/49abc3923b248651f4cb67b8d346c13f2691d622/FOSDEM2024/3D_print/cmkhexagon.stl -------------------------------------------------------------------------------- /FOSDEM2024/3D_print/cmkhexagonbase.scad: -------------------------------------------------------------------------------- 1 | $fn = 180; 2 | 3 | difference() { 4 | hull() { 5 | translate([0,0,0]) cube([50,40,1], true); 6 | translate([0,0,22]) cube([50,25,1], true); 7 | } 8 | union() { 9 | translate([-24,0,51.5]) rotate(a=[0,90,0]) cylinder(50, 40, 40); 10 | translate([-26,0,51.5]) rotate(a=[0,90,0]) cylinder(r=50, h=11, $fn=6); 11 | translate([-26,0,2]) rotate(a=[0,90,0]) cylinder(11, 0.75, 0.75); 12 | } 13 | } -------------------------------------------------------------------------------- /FOSDEM2024/3D_print/cmkhexagonbase.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschlenker/checkmk-snippets/49abc3923b248651f4cb67b8d346c13f2691d622/FOSDEM2024/3D_print/cmkhexagonbase.stl -------------------------------------------------------------------------------- /FOSDEM2024/3D_print/cmkhexagondrill.scad: -------------------------------------------------------------------------------- 1 | $fn = 180; 2 | 3 | difference() { 4 | difference() { 5 | translate([-0,0,51.5]) rotate(a=[0,90,0]) cylinder(50, 42.1, 42.1); 6 | union() { 7 | translate([40,0,51.5]) rotate(a=[30,0,0]) translate([0,0,-75]) cylinder(150, 2.2, 2.2); 8 | translate([40,0,51.5]) rotate(a=[90,0,0]) translate([0,0,-75]) cylinder(150, 2.2, 2.2); 9 | translate([40,0,51.5]) rotate(a=[-30,0,0]) translate([0,0,-75]) cylinder(150, 2.2, 2.2); 10 | translate([-0.1,0,51.5]) rotate(a=[0,90,0]) cylinder(70, 40.1, 40.1); 11 | } 12 | } 13 | difference() { 14 | hull() { 15 | translate([0,0,0]) cube([110,40,1], true); 16 | translate([0,0,22]) cube([110,25,1], true); 17 | } 18 | union() { 19 | translate([-24,0,51.5]) rotate(a=[0,90,0]) cylinder(50, 40, 40); 20 | translate([-26,0,51.5]) rotate(a=[0,90,0]) cylinder(r=50, h=11, $fn=6); 21 | translate([-26,0,2]) rotate(a=[0,90,0]) cylinder(11, 0.75, 0.75); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /FOSDEM2024/3D_print/cmkhexagondrill.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschlenker/checkmk-snippets/49abc3923b248651f4cb67b8d346c13f2691d622/FOSDEM2024/3D_print/cmkhexagondrill.stl -------------------------------------------------------------------------------- /FOSDEM2024/3D_print/cmkhexagonspacer.scad: -------------------------------------------------------------------------------- 1 | $fn = 180; 2 | 3 | difference() { 4 | union() { 5 | translate([0,0,20]) cube([15,15,40], true); 6 | 7 | } 8 | union() { 9 | translate([0,44.5,-1]) cylinder(50, 40, 40); 10 | translate([0,-44.5,-1]) cylinder(50, 40, 40); 11 | translate([0,25,10]) rotate(a=[90,0,0]) cylinder(50, 2.25, 2.25); 12 | /* translate([-24,0,51.5]) rotate(a=[0,90,0]) cylinder(50, 40, 40); 13 | translate([-26,0,51.5]) rotate(a=[0,90,0]) cylinder(r=50, h=11, $fn=6); 14 | translate([-26,0,2]) rotate(a=[0,90,0]) cylinder(11, 0.75, 0.75); */ 15 | } 16 | } -------------------------------------------------------------------------------- /FOSDEM2024/3D_print/cmkhexagonspacer.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschlenker/checkmk-snippets/49abc3923b248651f4cb67b8d346c13f2691d622/FOSDEM2024/3D_print/cmkhexagonspacer.stl -------------------------------------------------------------------------------- /FOSDEM2024/3D_print/cmkhexpcbmount.scad: -------------------------------------------------------------------------------- 1 | $fn = 180; 2 | 3 | difference() { 4 | hull() { 5 | cylinder(7.4, 3.7, 3.7); 6 | translate([-3.7,-5.2,3.1]) rotate(a=[0,90,0]) cylinder(7.4, 3.1, 3.1); 7 | translate([-0,-4.1,0.5]) cube(size=[7.4,6.2,1], center=true); 8 | } 9 | union() { 10 | translate([-0,-0,-0.1]) cylinder(7.8, 2.1, 2.1); 11 | translate([-3.8,-5.2,3.1]) rotate(a=[0,90,0]) cylinder(7.8, 1.5, 1.5); 12 | } 13 | } -------------------------------------------------------------------------------- /FOSDEM2024/3D_print/cmkhexpcbmount.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschlenker/checkmk-snippets/49abc3923b248651f4cb67b8d346c13f2691d622/FOSDEM2024/3D_print/cmkhexpcbmount.stl -------------------------------------------------------------------------------- /FOSDEM2024/Arduino/CO2-Ampel/examples/BMP280_Test/BMP208_Test.ino: -------------------------------------------------------------------------------- 1 | /* 2 | BMP280 Test 3 | 4 | Test progam for BMP280 sensor. 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | Adafruit_BMP280 bmp280(&Wire1); //sensor on Wire1 (2nd I2C port) 12 | 13 | void setup() 14 | { 15 | // LED 16 | pinMode(PIN_LED, OUTPUT); 17 | digitalWrite(PIN_LED, LOW); //LED off 18 | 19 | // init serial library 20 | Serial.begin(9600); 21 | while(!Serial); // wait for serial monitor 22 | Serial.println("BMP280"); 23 | 24 | digitalWrite(PIN_LED, HIGH); //LED on 25 | 26 | // BMP280 27 | if(bmp280.begin(0x76) != 0) 28 | { 29 | Serial.println("Detected as 0x76"); 30 | } 31 | else if(bmp280.begin(0x77) != 0) 32 | { 33 | Serial.println("Detected as 0x77"); 34 | } 35 | else 36 | { 37 | Serial.println("Error - Not Found"); 38 | return; // don't continue 39 | } 40 | } 41 | 42 | void loop() 43 | { 44 | float t=0, p=0, a=0; 45 | 46 | digitalWrite(PIN_LED, HIGH); //LED on 47 | 48 | t = bmp280.readTemperature(); 49 | p = bmp280.readPressure(); 50 | a = bmp280.readAltitude(1013.25); //1013.25 = sea level pressure 51 | 52 | digitalWrite(PIN_LED, LOW); //LED off 53 | 54 | Serial.print("Temp "); 55 | Serial.print(t, DEC); 56 | Serial.println(" *C"); 57 | Serial.print("Pres "); 58 | Serial.print(p/100.0, DEC); 59 | Serial.println(" hPa"); 60 | Serial.print("Alti "); 61 | Serial.print(a, DEC); 62 | Serial.println(" m"); 63 | Serial.println(""); 64 | 65 | delay(2000); 66 | } 67 | -------------------------------------------------------------------------------- /FOSDEM2024/Arduino/CO2-Ampel/examples/Blink/Blink.ino: -------------------------------------------------------------------------------- 1 | /* 2 | Blink 3 | 4 | Turns a LED on for one second, then off for one second, repeatedly. 5 | */ 6 | 7 | int ledPin = LED_BUILTIN; // LED pin, on-board LED (D3) 8 | 9 | // the setup function runs once when you press reset or power the board 10 | void setup() 11 | { 12 | // initialize digital pin as an output 13 | pinMode(ledPin, OUTPUT); 14 | } 15 | 16 | // the loop function runs over and over again forever 17 | void loop() 18 | { 19 | digitalWrite(ledPin, HIGH); // turn the LED on (HIGH is the voltage level) 20 | delay(1000); // wait for a second 21 | digitalWrite(ledPin, LOW); // turn the LED off by making the voltage LOW 22 | delay(1000); // wait for a second 23 | } 24 | -------------------------------------------------------------------------------- /FOSDEM2024/Arduino/CO2-Ampel/examples/Button-Switch/Button-Switch.ino: -------------------------------------------------------------------------------- 1 | /* 2 | Button/Switch 3 | 4 | Turns a LED on when pressing the pushbutton "Switch". 5 | */ 6 | 7 | int buttonPin = 2; // button pin 2 (switch) 8 | int ledPin = LED_BUILTIN; // LED pin, on-board LED 7 or 8 9 | 10 | // the setup function runs once when you press reset or power the board 11 | void setup() 12 | { 13 | // initialize digital pins 14 | pinMode(buttonPin, INPUT_PULLUP); 15 | pinMode(ledPin, OUTPUT); 16 | } 17 | 18 | // the loop function runs over and over again forever 19 | void loop() 20 | { 21 | int buttonState; // variable for reading the button status 22 | 23 | // read the state of the button 24 | buttonState = digitalRead(buttonPin); 25 | 26 | if(buttonState == LOW) // state low = button pressed 27 | { 28 | digitalWrite(ledPin, HIGH); // turn the LED on (HIGH is the voltage level) 29 | } 30 | else 31 | { 32 | digitalWrite(ledPin, LOW); // turn the LED off by making the voltage LOW 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /FOSDEM2024/Arduino/CO2-Ampel/examples/CO2-Ampel/CO2-Ampel.ino.co2ampel.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschlenker/checkmk-snippets/49abc3923b248651f4cb67b8d346c13f2691d622/FOSDEM2024/Arduino/CO2-Ampel/examples/CO2-Ampel/CO2-Ampel.ino.co2ampel.bin -------------------------------------------------------------------------------- /FOSDEM2024/Arduino/CO2-Ampel/examples/CO2-Ampel/CO2-Ampel.ino.with_bootloader.co2ampel.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschlenker/checkmk-snippets/49abc3923b248651f4cb67b8d346c13f2691d622/FOSDEM2024/Arduino/CO2-Ampel/examples/CO2-Ampel/CO2-Ampel.ino.with_bootloader.co2ampel.bin -------------------------------------------------------------------------------- /FOSDEM2024/Arduino/CO2-Ampel/examples/ECC608_Test/ECC608_Test.ino: -------------------------------------------------------------------------------- 1 | /* 2 | ECC608 Test 3 | 4 | Test progam for Microchip ATECC608, connected to Wire1 (I2C). 5 | */ 6 | 7 | #include 8 | #include 9 | 10 | // I2C address 11 | #define I2C_ADR (0x60) //0x60 12 | 13 | // I2C commands/registers 14 | #define CMD_RESET (0x00) 15 | #define CMD_SLEEP (0x01) 16 | #define CMD_IDLE (0x02) 17 | #define CMD_SEND (0x03) 18 | 19 | // ECC commands 20 | #define ECC_READ (0x02) // read command 21 | #define ECC_WRITE (0x12) // write command 22 | #define ECC_INFO (0x30) // info command 23 | 24 | // zone and address parameters 25 | #define ECC_ZONE_CFG (0x00) // configuration zone 26 | #define ECC_ZONE_OTP (0x01) // otp zone 27 | #define ECC_ZONE_DAT (0x02) // data zone 28 | #define ECC_ZONE_MSK (0x03) // zone mask 29 | #define ECC_ZONE_CNT_FLAG (0x80) // 1=32 bytes, 0=4 bytes 30 | #define ECC_ADDR_MSK_CFG (0x001F) // address mask for configuration zone 31 | #define ECC_ADDR_MSK_OTP (0x000F) // address mask for otp zone 32 | #define ECC_ADDR_MSK (0x007F) // address mask 33 | 34 | void read(byte *data, byte max_len) 35 | { 36 | byte len; 37 | 38 | Wire1.requestFrom(I2C_ADR, 1); // request length 39 | while(Wire1.available() == 0); // wait for data bytes 40 | len = Wire1.read(); 41 | *data++ = len; 42 | if(len) 43 | { 44 | Wire1.requestFrom(I2C_ADR, len); // request x bytes 45 | while(Wire1.available() == 0); // wait for data bytes 46 | delay(10); // wait 10ms 47 | for(byte i = 0; (i < len) && (i < max_len); i++) 48 | { 49 | *data++ = Wire1.read(); // read data byte 50 | } 51 | } 52 | } 53 | 54 | void write(byte reg, byte *data, byte len) 55 | { 56 | Wire1.beginTransmission(I2C_ADR); // start transmission 57 | Wire1.write(reg); // write register byte 58 | for(; len != 0; len--) 59 | { 60 | Wire1.write(*data++); // write data byte 61 | } 62 | Wire1.endTransmission(); // stop transmission 63 | } 64 | 65 | void write(byte reg, byte data) 66 | { 67 | Wire1.beginTransmission(I2C_ADR); // start transmission 68 | Wire1.write(reg); // write register byte 69 | Wire1.write(data); // write data byte 70 | Wire1.endTransmission(); // stop transmission 71 | } 72 | 73 | void calc_crc(byte *data, byte len, byte *crc) 74 | { 75 | uint8_t i, shift_reg, data_bit, crc_bit; 76 | uint16_t crc_reg = 0; 77 | uint16_t polynom = 0x8005; 78 | 79 | for(i = 0; i < len; i++) 80 | { 81 | for(shift_reg = 0x01; shift_reg > 0x00; shift_reg <<= 1) 82 | { 83 | data_bit = (data[i] & shift_reg) ? 1 : 0; 84 | crc_bit = crc_reg >> 15; 85 | crc_reg <<= 1; 86 | if(data_bit != crc_bit) 87 | { 88 | crc_reg ^= polynom; 89 | } 90 | } 91 | } 92 | crc[0] = (byte)(crc_reg & 0x00FF); 93 | crc[1] = (byte)(crc_reg >> 8); 94 | } 95 | 96 | void setup() 97 | { 98 | // init serial library 99 | Serial.begin(9600); 100 | while(!Serial); // wait for serial monitor 101 | Serial.println("ECC608"); 102 | 103 | // init I2C/Wire library 104 | Wire1.begin(); 105 | 106 | // test ArduinoECCX08 lib 107 | /* 108 | ECCX08Class ecc(Wire1, 0x60); 109 | if(ecc.begin()) 110 | { 111 | if(ecc.locked()) 112 | { 113 | Serial.print("Random: "); 114 | Serial.println(ecc.random(65535)); // random(max) 115 | } 116 | else 117 | { 118 | Serial.println("not locked"); 119 | } 120 | } 121 | */ 122 | 123 | // init ATECC 124 | write(CMD_RESET, 0x00); // reset 125 | delay(100); // wait 100ms 126 | 127 | // read config zone 128 | byte buf[64]; // buffer 129 | 130 | buf[0] = 5+2; // length: data + 2 crc bytes 131 | buf[1] = ECC_READ; // cmd 132 | buf[2] = ECC_ZONE_CFG|ECC_ZONE_CNT_FLAG; // param 1 133 | buf[3] = 0x00; // addr lsb 134 | buf[4] = 0x00; // addr msb 135 | //buf[5] = 0x00; // crc 136 | //buf[6] = 0x00; // crc 137 | calc_crc(buf, buf[0]-2, &buf[5]); // calc crc 138 | 139 | write(CMD_SEND, buf, buf[0]); // send cmd 140 | delay(10); // wait 10ms 141 | read(buf, sizeof(buf)); // read response 142 | 143 | Serial.print("Len: "); 144 | Serial.print(buf[0], HEX); Serial.print(" "); 145 | Serial.println(""); 146 | 147 | Serial.print("ID: "); 148 | Serial.print(buf[1], HEX); Serial.print(" "); 149 | Serial.print(buf[2], HEX); Serial.print(" "); 150 | Serial.print(buf[3], HEX); Serial.print(" "); 151 | Serial.print(buf[4], HEX); Serial.print(" "); 152 | Serial.println(""); 153 | 154 | Serial.print("Rev: "); 155 | Serial.print(buf[5], HEX); Serial.print(" "); 156 | Serial.print(buf[6], HEX); Serial.print(" "); 157 | Serial.print(buf[7], HEX); Serial.print(" "); 158 | Serial.print(buf[8], HEX); Serial.print(" "); 159 | Serial.println(""); 160 | 161 | Serial.print("SN: "); 162 | Serial.print(buf[ 9], HEX); Serial.print(" "); 163 | Serial.print(buf[10], HEX); Serial.print(" "); 164 | Serial.print(buf[11], HEX); Serial.print(" "); 165 | Serial.print(buf[12], HEX); Serial.print(" "); 166 | Serial.print(buf[13], HEX); Serial.print(" "); 167 | Serial.println(""); 168 | 169 | Serial.print("EE: "); 170 | Serial.print(buf[14], HEX); Serial.print(" "); 171 | Serial.println(""); 172 | 173 | Serial.print("I2C: "); 174 | Serial.print(buf[15], HEX); Serial.print(" "); 175 | Serial.println(""); 176 | 177 | Serial.print("EnLock: "); 178 | Serial.print(buf[16], HEX); Serial.print(" "); 179 | Serial.println(""); 180 | 181 | Serial.print("TWI: "); 182 | Serial.print(buf[17], HEX); Serial.print(" "); 183 | Serial.println(""); 184 | } 185 | 186 | void loop() 187 | { 188 | // do nothing 189 | } 190 | -------------------------------------------------------------------------------- /FOSDEM2024/Arduino/CO2-Ampel/examples/ECCX08_CSR/ECCX08_CSR.ino: -------------------------------------------------------------------------------- 1 | /* 2 | ArduinoECCX08 - CSR (Certificate Signing Request) 3 | 4 | This sketch can be used to generate a CSR for a private key 5 | generated in an ECC508/ECC608 crypto chip slot. 6 | 7 | If the ECC508/ECC608 is not configured and locked it prompts 8 | the user to configure and lock the chip with a default TLS 9 | configuration. 10 | 11 | The user is prompted for the following information that is contained 12 | in the generated CSR: 13 | - country 14 | - state or province 15 | - locality 16 | - organization 17 | - organizational unit 18 | - common name 19 | 20 | The user can also select a slot number to use for the private key 21 | A new private key can also be generated in this slot. 22 | 23 | The circuit: 24 | - Arduino MKR board equipped with ECC508 or ECC608 chip 25 | 26 | This example code is in the public domain. 27 | */ 28 | 29 | #include 30 | #include 31 | #include 32 | 33 | void setup() { 34 | Serial.begin(9600); 35 | while (!Serial); 36 | 37 | if (!ECCX08.begin()) { 38 | Serial.println("No ECCX08 present!"); 39 | while (1); 40 | } 41 | 42 | String serialNumber = ECCX08.serialNumber(); 43 | 44 | Serial.print("ECCX08 Serial Number = "); 45 | Serial.println(serialNumber); 46 | Serial.println(); 47 | 48 | if (!ECCX08.locked()) { 49 | String lock = promptAndReadLine("The ECCX08 on your board is not locked, would you like to configure and lock it now? (y/N)", "N"); 50 | 51 | if (!lock.startsWith("y")) { 52 | Serial.println("Unfortunately you can't proceed without locking it :("); 53 | while (1); 54 | } 55 | 56 | if (!ECCX08.writeConfiguration(ECCX08_DEFAULT_TLS_CONFIG)) { 57 | Serial.println("Writing ECCX08 configuration failed!"); 58 | while (1); 59 | } 60 | 61 | if (!ECCX08.lock()) { 62 | Serial.println("Locking ECCX08 configuration failed!"); 63 | while (1); 64 | } 65 | 66 | Serial.println("ECCX08 locked successfully"); 67 | Serial.println(); 68 | } 69 | 70 | Serial.println("Hi there, in order to generate a new CSR for your board, we'll need the following information ..."); 71 | Serial.println(); 72 | 73 | String country = promptAndReadLine("Country Name (2 letter code)", ""); 74 | String stateOrProvince = promptAndReadLine("State or Province Name (full name)", ""); 75 | String locality = promptAndReadLine("Locality Name (eg, city)", ""); 76 | String organization = promptAndReadLine("Organization Name (eg, company)", ""); 77 | String organizationalUnit = promptAndReadLine("Organizational Unit Name (eg, section)", ""); 78 | String common = promptAndReadLine("Common Name (e.g. server FQDN or YOUR name)", serialNumber.c_str()); 79 | String slot = promptAndReadLine("What slot would you like to use? (0 - 4)", "0"); 80 | String generateNewKey = promptAndReadLine("Would you like to generate a new private key? (Y/n)", "Y"); 81 | 82 | Serial.println(); 83 | 84 | generateNewKey.toLowerCase(); 85 | 86 | if (!ECCX08CSR.begin(slot.toInt(), generateNewKey.startsWith("y"))) { 87 | Serial.println("Error starting CSR generation!"); 88 | while (1); 89 | } 90 | 91 | ECCX08CSR.setCountryName(country); 92 | ECCX08CSR.setStateProvinceName(stateOrProvince); 93 | ECCX08CSR.setLocalityName(locality); 94 | ECCX08CSR.setOrganizationName(organization); 95 | ECCX08CSR.setOrganizationalUnitName(organizationalUnit); 96 | ECCX08CSR.setCommonName(common); 97 | 98 | String csr = ECCX08CSR.end(); 99 | 100 | if (!csr) { 101 | Serial.println("Error generating CSR!"); 102 | while (1); 103 | } 104 | 105 | Serial.println("Here's your CSR, enjoy!"); 106 | Serial.println(); 107 | Serial.println(csr); 108 | } 109 | 110 | void loop() { 111 | // do nothing 112 | } 113 | 114 | String promptAndReadLine(const char* prompt, const char* defaultValue) { 115 | Serial.print(prompt); 116 | Serial.print(" ["); 117 | Serial.print(defaultValue); 118 | Serial.print("]: "); 119 | 120 | String s = readLine(); 121 | 122 | if (s.length() == 0) { 123 | s = defaultValue; 124 | } 125 | 126 | Serial.println(s); 127 | 128 | return s; 129 | } 130 | 131 | String readLine() { 132 | String line; 133 | 134 | while (1) { 135 | if (Serial.available()) { 136 | char c = Serial.read(); 137 | 138 | if (c == '\r') { 139 | // ignore 140 | continue; 141 | } else if (c == '\n') { 142 | break; 143 | } 144 | 145 | line += c; 146 | } 147 | } 148 | 149 | return line; 150 | } 151 | -------------------------------------------------------------------------------- /FOSDEM2024/Arduino/CO2-Ampel/examples/I2C-Scanner/I2C-Scanner.ino: -------------------------------------------------------------------------------- 1 | /* 2 | I2C-Scanner 3 | 4 | Scans the I2C Bus for devices. 5 | */ 6 | 7 | #include 8 | 9 | void setup() 10 | { 11 | // init serial library 12 | Serial.begin(9600); 13 | while(!Serial); // wait for serial monitor 14 | Serial.println("I2C Scanner"); 15 | 16 | // init I2C/Wire library 17 | Wire.begin(); 18 | } 19 | 20 | void loop() 21 | { 22 | byte devices, address; 23 | 24 | Serial.println("Scanning..."); 25 | 26 | devices = 0; 27 | for(address = 1; address < 127; address++ ) 28 | { 29 | Wire.beginTransmission(address); 30 | byte error = Wire.endTransmission(); 31 | 32 | if(error == 0) 33 | { 34 | devices++; 35 | Serial.print("Device found at 0x"); 36 | Serial.println(address, HEX); 37 | } 38 | else if(error == 4) 39 | { 40 | Serial.print("Unknow error at 0x"); 41 | Serial.println(address, HEX); 42 | } 43 | } 44 | 45 | if(devices == 0) 46 | { 47 | Serial.println("No devices found\n"); 48 | } 49 | else 50 | { 51 | Serial.println("done\n"); 52 | } 53 | 54 | delay(5000); // wait 5 seconds for next scan 55 | } 56 | -------------------------------------------------------------------------------- /FOSDEM2024/Arduino/CO2-Ampel/examples/RFM9X_TTN/RFM9X_TTN.ino: -------------------------------------------------------------------------------- 1 | /* 2 | RFM9X TTN Test 3 | 4 | Test progam for RFM9X (LoRa-Module). 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | // LoRaWAN NwkSKey, network session key 12 | // This is the default Semtech key, which is used by the early prototype TTN network. 13 | static u1_t NWKSKEY[16] = { 0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6, 0xAB, 0xF7, 0x15, 0x88, 0x09, 0xCF, 0x4F, 0x3C }; 14 | 15 | // LoRaWAN AppSKey, application session key 16 | // This is the default Semtech key, which is used by the early prototype TTN network. 17 | static u1_t APPSKEY[16] = { 0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6, 0xAB, 0xF7, 0x15, 0x88, 0x09, 0xCF, 0x4F, 0x3C }; 18 | 19 | // LoRaWAN end-device address (DevAddr) 20 | static u4_t DEVADDR = 0x03FF0001 ; // <-- Change this address for every node! 21 | 22 | // these callbacks are only used in over-the-air activation, so they are left empty here 23 | void os_getArtEui (u1_t* buf) { } 24 | void os_getDevEui (u1_t* buf) { } 25 | void os_getDevKey (u1_t* buf) { } 26 | 27 | static uint8_t mydata[] = "Hello, world!"; 28 | static osjob_t sendjob; 29 | 30 | // schedule TX every this many seconds (might become longer due to duty cycle limitations). 31 | const unsigned TX_INTERVAL = 60; 32 | 33 | // pin mapping 34 | const lmic_pinmap lmic_pins = { 35 | .nss = 20, /* RFM9X CS */ 36 | .rxtx = LMIC_UNUSED_PIN, 37 | .rst = LMIC_UNUSED_PIN, 38 | .dio = {21, 22, LMIC_UNUSED_PIN}, /* DIO0, DIO1 */ 39 | }; 40 | 41 | void onEvent(ev_t ev) 42 | { 43 | digitalWrite(PIN_LED, HIGH); 44 | Serial.print(os_getTime()); 45 | Serial.print(": "); 46 | switch(ev) 47 | { 48 | case EV_SCAN_TIMEOUT: 49 | Serial.println(F("EV_SCAN_TIMEOUT")); 50 | break; 51 | case EV_BEACON_FOUND: 52 | Serial.println(F("EV_BEACON_FOUND")); 53 | break; 54 | case EV_BEACON_MISSED: 55 | Serial.println(F("EV_BEACON_MISSED")); 56 | break; 57 | case EV_BEACON_TRACKED: 58 | Serial.println(F("EV_BEACON_TRACKED")); 59 | break; 60 | case EV_JOINING: 61 | Serial.println(F("EV_JOINING")); 62 | break; 63 | case EV_JOINED: 64 | Serial.println(F("EV_JOINED")); 65 | break; 66 | case EV_RFU1: 67 | Serial.println(F("EV_RFU1")); 68 | break; 69 | case EV_JOIN_FAILED: 70 | Serial.println(F("EV_JOIN_FAILED")); 71 | break; 72 | case EV_REJOIN_FAILED: 73 | Serial.println(F("EV_REJOIN_FAILED")); 74 | break; 75 | case EV_TXCOMPLETE: 76 | Serial.println(F("EV_TXCOMPLETE (includes waiting for RX windows)")); 77 | if(LMIC.txrxFlags & TXRX_ACK) 78 | { 79 | Serial.println(F("Received ack")); 80 | } 81 | if(LMIC.dataLen) 82 | { 83 | Serial.println(F("Received ")); 84 | Serial.println(LMIC.dataLen); 85 | Serial.println(F(" bytes of payload")); 86 | } 87 | // schedule next transmission 88 | os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send); 89 | break; 90 | case EV_LOST_TSYNC: 91 | Serial.println(F("EV_LOST_TSYNC")); 92 | break; 93 | case EV_RESET: 94 | Serial.println(F("EV_RESET")); 95 | break; 96 | case EV_RXCOMPLETE: 97 | // data received in ping slot 98 | Serial.println(F("EV_RXCOMPLETE")); 99 | break; 100 | case EV_LINK_DEAD: 101 | Serial.println(F("EV_LINK_DEAD")); 102 | break; 103 | case EV_LINK_ALIVE: 104 | Serial.println(F("EV_LINK_ALIVE")); 105 | break; 106 | default: 107 | Serial.println(F("Unknown event")); 108 | break; 109 | } 110 | digitalWrite(PIN_LED, LOW); 111 | } 112 | 113 | void do_send(osjob_t* j) 114 | { 115 | digitalWrite(PIN_LED, HIGH); 116 | // sheck if there is not a current TX/RX job running 117 | if(LMIC.opmode & OP_TXRXPEND) 118 | { 119 | Serial.println(F("OP_TXRXPEND, not sending")); 120 | } 121 | else 122 | { 123 | // prepare upstream data transmission at the next possible time 124 | LMIC_setTxData2(1, mydata, sizeof(mydata)-1, 0); 125 | Serial.println(F("Packet queued")); 126 | } 127 | // next TX is scheduled after TX_COMPLETE event 128 | digitalWrite(PIN_LED, LOW); 129 | } 130 | 131 | void setup() 132 | { 133 | // LED 134 | pinMode(PIN_LED, OUTPUT); 135 | digitalWrite(PIN_LED, LOW); //LED off 136 | 137 | // init serial library 138 | Serial.begin(9600); 139 | while(!Serial); // wait for serial monitor 140 | Serial.println("RFM9X TTN"); 141 | 142 | digitalWrite(PIN_LED, HIGH); //LED on 143 | 144 | // RFM9X 145 | pinMode(20, OUTPUT); //CS 146 | digitalWrite(20, HIGH); 147 | pinMode(21, INPUT_PULLDOWN); // DIO0, pull-down because interrupt is high-active 148 | pinMode(22, INPUT_PULLDOWN); // DIO1, pull-down because interrupt is high-active 149 | 150 | // initialize runtime env 151 | os_init(); 152 | 153 | // reset the MAC state 154 | LMIC_reset(); 155 | 156 | // set static session parameters 157 | LMIC_setSession (0x1, DEVADDR, NWKSKEY, APPSKEY); 158 | 159 | // set up the channels 160 | LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band 161 | LMIC_setupChannel(1, 868300000, DR_RANGE_MAP(DR_SF12, DR_SF7B), BAND_CENTI); // g-band 162 | LMIC_setupChannel(2, 868500000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band 163 | LMIC_setupChannel(3, 867100000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band 164 | LMIC_setupChannel(4, 867300000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band 165 | LMIC_setupChannel(5, 867500000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band 166 | LMIC_setupChannel(6, 867700000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band 167 | LMIC_setupChannel(7, 867900000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band 168 | LMIC_setupChannel(8, 868800000, DR_RANGE_MAP(DR_FSK, DR_FSK), BAND_MILLI); // g2-band 169 | 170 | // disable link check validation 171 | LMIC_setLinkCheckMode(0); 172 | 173 | // TTN uses SF9 for its RX2 window. 174 | LMIC.dn2Dr = DR_SF9; 175 | 176 | // det data rate and transmit power for uplink (note: txpow seems to be ignored by the library) 177 | LMIC_setDrTxpow(DR_SF7, 14); 178 | 179 | // start job 180 | do_send(&sendjob); 181 | } 182 | 183 | void loop() 184 | { 185 | // execute scheduled jobs and events 186 | os_runloop_once(); 187 | } 188 | -------------------------------------------------------------------------------- /FOSDEM2024/Arduino/CO2-Ampel/examples/RFM9X_Test/RFM9X_Test.ino: -------------------------------------------------------------------------------- 1 | /* 2 | RFM9X Test 3 | 4 | Test progam for RFM9X (LoRa-Module). 5 | */ 6 | 7 | #include 8 | 9 | #define CS_PIN 20 //CS pin from RFM9X 10 | 11 | void setup() 12 | { 13 | // LED 14 | pinMode(PIN_LED, OUTPUT); 15 | digitalWrite(PIN_LED, LOW); //LED off 16 | 17 | // init serial library 18 | Serial.begin(9600); 19 | while(!Serial); // wait for serial monitor 20 | Serial.println("RFM9X"); 21 | 22 | digitalWrite(PIN_LED, HIGH); //LED on 23 | 24 | // RFM9X 25 | pinMode(CS_PIN, OUTPUT); 26 | digitalWrite(CS_PIN, HIGH); 27 | pinMode(21, INPUT_PULLDOWN); // DIO0, pull-down because interrupt is high-active 28 | pinMode(22, INPUT_PULLDOWN); // DIO1, pull-down because interrupt is high-active 29 | 30 | // init SPI 31 | SPI.begin(); 32 | SPI.setDataMode(SPI_MODE0); 33 | SPI.setBitOrder(MSBFIRST); 34 | SPI.setClockDivider(SPI_CLOCK_DIV128); 35 | 36 | // read version 37 | digitalWrite(CS_PIN, LOW); 38 | SPI.transfer(0x42); // 0x42 = version 39 | byte i = SPI.transfer(0x00); // get value 40 | digitalWrite(CS_PIN, HIGH); 41 | 42 | // check version 43 | if(i != 0x12) 44 | { 45 | Serial.println("Error - Not Found"); 46 | return; // don't continue 47 | } 48 | Serial.println("OK - Detected"); 49 | } 50 | 51 | void loop() 52 | { 53 | // do nothing 54 | } 55 | -------------------------------------------------------------------------------- /FOSDEM2024/Arduino/CO2-Ampel/examples/SSL_Test/SSL_Test.ino: -------------------------------------------------------------------------------- 1 | /* 2 | SSL/TLS Test 3 | 4 | Test progam for SSL (WINC1500). 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include "WiFiUdp.h" 11 | #include 12 | #include 13 | 14 | // WiFi/WLAN settings 15 | char ssid[] = "yourNetwork"; // your network SSID (name) 16 | char pass[] = "yourPassword"; // your network password 17 | 18 | IPAddress ip(192,168,1,177); // IP if DHCP fails 19 | #define website_for_test "www.sensebox.de" // website used for connection test 20 | 21 | WiFiClient client; // remote client 22 | BearSSLClient sslClient(client); // remote SSL client 23 | 24 | unsigned long getTime() 25 | { 26 | WiFiUDP udp; 27 | 28 | uint8_t pkt[48]; 29 | unsigned long time; 30 | 31 | memset(pkt, 0, 48); 32 | pkt[0] = 0xE3; // LI, version, mode 33 | pkt[1] = 0x00; // stratum, or type of clock 34 | pkt[2] = 0x06; // polling interval 35 | pkt[3] = 0xEC; // peer clock precision 36 | 37 | udp.begin(8888); // local port 8888 38 | udp.beginPacket("pool.ntp.org", 123); // NTP request to port 123 39 | udp.write(pkt, 48); // write NTP packet 40 | udp.endPacket(); 41 | 42 | delay(1000); //wait 1s 43 | 44 | // read packet 45 | if(udp.parsePacket() == 0) 46 | { 47 | return 0; 48 | } 49 | udp.read(pkt, 48); 50 | 51 | // calculate seconds sice 1970 52 | time = ((pkt[40]<<8+16)|(pkt[41]<<0+16)) | ((pkt[42]<<8+0)|(pkt[43]<<0+0)); // seconds since 1900 53 | time -= 2208988800UL; //seconds: 1900-1970 54 | 55 | return time; 56 | } 57 | 58 | void setup() 59 | { 60 | // init serial library 61 | Serial.begin(9600); 62 | while(!Serial); // wait for serial monitor 63 | Serial.println("SSL/TLS Test"); 64 | 65 | // init WiFi lib 66 | if(WiFi.status() == WL_NO_SHIELD) 67 | { 68 | WiFi.end(); 69 | return; // don't continue 70 | } 71 | while(WiFi.begin(ssid, pass) != WL_CONNECTED) // connect to WiFi network 72 | { 73 | delay(3000); // wait 3 seconds 74 | } 75 | ip = WiFi.localIP(); 76 | Serial.print("IP: "); 77 | Serial.println(ip); 78 | 79 | // get time 80 | time_t t = getTime(); 81 | Serial.print("Time: "); 82 | Serial.println(ctime(&t)); 83 | 84 | // init ArduinoBearSSL lib 85 | ArduinoBearSSL.onGetTime(getTime); 86 | 87 | // test remote connection 88 | Serial.println("--- Starting normal/unsecured connection ---"); 89 | if(client.connect(website_for_test, 80)) 90 | { 91 | Serial.println("Connection OK - "website_for_test); 92 | client.println("GET / HTTP/1.0"); 93 | client.println("Host: "website_for_test); 94 | client.println("Connection: close"); 95 | client.println(); 96 | delay(2000); //wait 2s 97 | //show response 98 | while(client.available()) 99 | { 100 | char c = client.read(); 101 | Serial.write(c); 102 | } 103 | } 104 | else 105 | { 106 | Serial.println("Error"); 107 | } 108 | client.stop(); 109 | 110 | // test remote SSL connection 111 | Serial.println("\n\n--- Starting SSL connection ---"); 112 | if(sslClient.connect(website_for_test, 443)) 113 | { 114 | Serial.println("Connection OK - "website_for_test); 115 | sslClient.println("GET / HTTP/1.0"); 116 | sslClient.println("Host: "website_for_test); 117 | sslClient.println("Connection: close"); 118 | sslClient.println(); 119 | delay(2000); //wait 2s 120 | //show response 121 | while(sslClient.available()) 122 | { 123 | char c = sslClient.read(); 124 | Serial.write(c); 125 | } 126 | } 127 | else 128 | { 129 | Serial.println("Error"); 130 | } 131 | sslClient.stop(); 132 | } 133 | 134 | void loop() 135 | { 136 | // do nothing 137 | } 138 | -------------------------------------------------------------------------------- /FOSDEM2024/Arduino/CO2-Ampel/examples/WINC1500_Test/WINC1500_Test.ino: -------------------------------------------------------------------------------- 1 | /* 2 | WINC1500 Test 3 | 4 | Test progam for WINC1500. 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | WiFiServer server(80); // local webserver on port 80 12 | 13 | void setup() 14 | { 15 | // init serial library 16 | Serial.begin(9600); 17 | while(!Serial); // wait for serial monitor 18 | Serial.println("WINC1500"); 19 | 20 | // init WINC1500 21 | if(WiFi.status() == WL_NO_SHIELD) 22 | { 23 | Serial.println("Error - Not Found"); 24 | // shutdown WINC1500 25 | WiFi.end(); 26 | return; // don't continue 27 | } 28 | Serial.println("OK - Detected"); 29 | 30 | // print firmware version 31 | String fv = WiFi.firmwareVersion(); 32 | Serial.print("Firmware installed: "); 33 | Serial.println(fv); 34 | Serial.print("Latest firmware: "); 35 | Serial.println(WIFI_FIRMWARE_LATEST_MODEL_B); //WIFI_FIRMWARE_LATEST_MODEL_B WIFI_FIRMWARE_LATEST_MODEL_A 36 | 37 | // create AP 38 | Serial.println("Creating access point 'Test-AP'"); 39 | int status = WiFi.beginAP("Test-AP"); 40 | if(status != WL_AP_LISTENING) 41 | { 42 | Serial.println("Error"); 43 | // shutdown WINC1500 44 | WiFi.end(); 45 | return; // don't continue 46 | } 47 | 48 | // start local webserver 49 | Serial.println("Starting Webserver..."); 50 | server.begin(); 51 | 52 | // print device IP 53 | IPAddress ip; 54 | ip = WiFi.localIP(); 55 | Serial.print("IP: "); 56 | Serial.println(ip); 57 | 58 | ip = WiFi.subnetMask(); 59 | Serial.print("NM: "); 60 | Serial.println(ip); 61 | 62 | ip = WiFi.gatewayIP(); 63 | Serial.print("GW: "); 64 | Serial.println(ip); 65 | } 66 | 67 | void loop() 68 | { 69 | // listen for incoming clients 70 | WiFiClient client = server.available(); 71 | if(client) 72 | { 73 | Serial.println("new client"); 74 | // an http request ends with a blank line 75 | boolean currentLineIsBlank = true; 76 | while(client.connected()) 77 | { 78 | if(client.available()) 79 | { 80 | char c = client.read(); 81 | Serial.write(c); 82 | // if you've gotten to the end of the line (received a newline 83 | // character) and the line is blank, the http request has ended, 84 | // so you can send a reply 85 | if(c == '\n' && currentLineIsBlank) 86 | { 87 | // send standard http response header 88 | client.println("HTTP/1.1 200 OK"); 89 | client.println("Content-Type: text/html"); 90 | client.println("Connection: close"); 91 | client.println(); 92 | // send html data 93 | client.println(""); 94 | client.println("Webserver"); 95 | break; 96 | } 97 | if(c == '\n') 98 | { 99 | // you're starting a new line 100 | currentLineIsBlank = true; 101 | } 102 | else if(c != '\r') 103 | { 104 | // you've gotten a character on the current line 105 | currentLineIsBlank = false; 106 | } 107 | } 108 | } 109 | // give the web browser time to receive the data 110 | delay(2); 111 | // close the connection 112 | client.stop(); 113 | Serial.println("client disconnected"); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /FOSDEM2024/Arduino/CO2-Ampel/examples/WINC1500_Updater/Endianess.ino: -------------------------------------------------------------------------------- 1 | /* 2 | Endianess.ino - Network byte order conversion functions. 3 | Copyright (c) 2015 Arduino LLC. All right reserved. 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18 | */ 19 | 20 | 21 | bool isBigEndian() { 22 | uint32_t test = 0x11223344; 23 | uint8_t *pTest = reinterpret_cast(&test); 24 | return pTest[0] == 0x11; 25 | } 26 | 27 | uint32_t fromNetwork32(uint32_t from) { 28 | static const bool be = isBigEndian(); 29 | if (be) { 30 | return from; 31 | } else { 32 | uint8_t *pFrom = reinterpret_cast(&from); 33 | uint32_t to; 34 | to = pFrom[0]; to <<= 8; 35 | to |= pFrom[1]; to <<= 8; 36 | to |= pFrom[2]; to <<= 8; 37 | to |= pFrom[3]; 38 | return to; 39 | } 40 | } 41 | 42 | uint16_t fromNetwork16(uint16_t from) { 43 | static bool be = isBigEndian(); 44 | if (be) { 45 | return from; 46 | } else { 47 | uint8_t *pFrom = reinterpret_cast(&from); 48 | uint16_t to; 49 | to = pFrom[0]; to <<= 8; 50 | to |= pFrom[1]; 51 | return to; 52 | } 53 | } 54 | 55 | uint32_t toNetwork32(uint32_t to) { 56 | return fromNetwork32(to); 57 | } 58 | 59 | uint16_t toNetwork16(uint16_t to) { 60 | return fromNetwork16(to); 61 | } 62 | 63 | -------------------------------------------------------------------------------- /FOSDEM2024/Arduino/CO2-Ampel/examples/WINC1500_Updater/WINC1500_Updater.ino: -------------------------------------------------------------------------------- 1 | /* 2 | FirmwareUpdate.h - Firmware Updater for WiFi101 / WINC1500. 3 | Copyright (c) 2015 Arduino LLC. All right reserved. 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18 | */ 19 | 20 | #include 21 | #include 22 | 23 | typedef struct __attribute__((__packed__)) { 24 | uint8_t command; 25 | uint32_t address; 26 | uint32_t arg1; 27 | uint16_t payloadLength; 28 | 29 | // payloadLenght bytes of data follows... 30 | } UartPacket; 31 | 32 | static const int MAX_PAYLOAD_SIZE = 1024; 33 | 34 | #define CMD_READ_FLASH 0x01 35 | #define CMD_WRITE_FLASH 0x02 36 | #define CMD_ERASE_FLASH 0x03 37 | #define CMD_MAX_PAYLOAD_SIZE 0x50 38 | #define CMD_HELLO 0x99 39 | 40 | void setup() { 41 | Serial.begin(115200); 42 | 43 | nm_bsp_init(); 44 | if (m2m_wifi_download_mode() != M2M_SUCCESS) { 45 | Serial.println(F("Failed to put the WiFi module in download mode")); 46 | while (true) 47 | ; 48 | } 49 | } 50 | 51 | void receivePacket(UartPacket *pkt, uint8_t *payload) { 52 | // Read command 53 | uint8_t *p = reinterpret_cast(pkt); 54 | uint16_t l = sizeof(UartPacket); 55 | while (l > 0) { 56 | int c = Serial.read(); 57 | if (c == -1) 58 | continue; 59 | *p++ = c; 60 | l--; 61 | } 62 | 63 | // Convert parameters from network byte order to cpu byte order 64 | pkt->address = fromNetwork32(pkt->address); 65 | pkt->arg1 = fromNetwork32(pkt->arg1); 66 | pkt->payloadLength = fromNetwork16(pkt->payloadLength); 67 | 68 | // Read payload 69 | l = pkt->payloadLength; 70 | while (l > 0) { 71 | int c = Serial.read(); 72 | if (c == -1) 73 | continue; 74 | *payload++ = c; 75 | l--; 76 | } 77 | } 78 | 79 | // Allocated statically so the compiler can tell us 80 | // about the amount of used RAM 81 | static UartPacket pkt; 82 | static uint8_t payload[MAX_PAYLOAD_SIZE]; 83 | 84 | void loop() { 85 | receivePacket(&pkt, payload); 86 | 87 | if (pkt.command == CMD_HELLO) { 88 | if (pkt.address == 0x11223344 && pkt.arg1 == 0x55667788) 89 | Serial.print("v10000"); 90 | } 91 | 92 | if (pkt.command == CMD_MAX_PAYLOAD_SIZE) { 93 | uint16_t res = toNetwork16(MAX_PAYLOAD_SIZE); 94 | Serial.write(reinterpret_cast(&res), sizeof(res)); 95 | } 96 | 97 | if (pkt.command == CMD_READ_FLASH) { 98 | uint32_t address = pkt.address; 99 | uint32_t len = pkt.arg1; 100 | if (spi_flash_read(payload, address, len) != M2M_SUCCESS) { 101 | Serial.println("ER"); 102 | } else { 103 | Serial.write(payload, len); 104 | Serial.print("OK"); 105 | } 106 | } 107 | 108 | if (pkt.command == CMD_WRITE_FLASH) { 109 | uint32_t address = pkt.address; 110 | uint32_t len = pkt.payloadLength; 111 | if (spi_flash_write(payload, address, len) != M2M_SUCCESS) { 112 | Serial.print("ER"); 113 | } else { 114 | Serial.print("OK"); 115 | } 116 | } 117 | 118 | if (pkt.command == CMD_ERASE_FLASH) { 119 | uint32_t address = pkt.address; 120 | uint32_t len = pkt.arg1; 121 | if (spi_flash_erase(address, len) != M2M_SUCCESS) { 122 | Serial.print("ER"); 123 | } else { 124 | Serial.print("OK"); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /FOSDEM2024/Arduino/CO2-Ampel/library.properties: -------------------------------------------------------------------------------- 1 | name=CO2-Ampel 2 | version=1.0.0 3 | author=Watterott electronic 4 | maintainer=Watterott electronic 5 | sentence=CO2-Ampel Examples 6 | paragraph=CO2-Ampel Examples 7 | category=Other 8 | url=https://learn.watterott.com/breakouts/co2-ampel/ 9 | architectures=samd 10 | -------------------------------------------------------------------------------- /FOSDEM2024/Arduino/CO2-Ampel/src/co2ampel.h: -------------------------------------------------------------------------------- 1 | //empty file, workaround for libraries containing only examples 2 | -------------------------------------------------------------------------------- /FOSDEM2024/Arduino/LoraReceiverTest/LoraReceiverTest.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | //---LoRa--- 5 | #define LORA_NODENAME "co2ampel4" //Wird mit den Messwerten übertragen 6 | #define LORA_TX_POWER 17 //Zwischen 2 und 20, Default 17 7 | #define LORA_FREQ 868E6 //868MHz, Europa 8 | #define LORA_INTERVALL 15000 //Millisekunden zwischen zwei Paketen, Duty Cycle beachten! 9 | #define LORA_SPREAD 7 //Spreading factor, beeinflusst Datenrate, 6-12, Default 7 10 | #define LORA_BANDWIDTH 125E3 //Bandwidth in Hz 11 | 12 | void setup() { 13 | 14 | Serial.begin(9600); 15 | while (!Serial); 16 | LoRa.setPins(20, -1); 17 | LoRa.enableCrc(); 18 | // Serial.println("LoRa Receiver"); 19 | 20 | if (!LoRa.begin(868E6)) { 21 | Serial.println("Starting LoRa failed!"); 22 | while (1); 23 | } 24 | LoRa.setSpreadingFactor(LORA_SPREAD); 25 | LoRa.setTxPower(LORA_TX_POWER); 26 | LoRa.setSignalBandwidth(LORA_BANDWIDTH); 27 | LoRa.enableCrc(); 28 | } 29 | 30 | void loop() { 31 | // try to parse packet 32 | int packetSize = LoRa.parsePacket(); 33 | if (packetSize) { 34 | // received a packet 35 | // Serial.print("Received packet '"); 36 | 37 | // read packet 38 | while (LoRa.available()) { 39 | Serial.print((char)LoRa.read()); 40 | } 41 | 42 | // print RSSI of packet 43 | // Serial.print("' with RSSI "); 44 | // Serial.println(LoRa.packetRssi()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /FOSDEM2024/Arduino/LoraSenderTest/LoraSenderTest.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int counter = 0; 5 | 6 | void setup() { 7 | // Serial.begin(9600); 8 | pinMode(20, OUTPUT); 9 | LoRa.setPins(20, 3); 10 | // digitalWrite(20, HIGH); 11 | // while (!Serial); 12 | 13 | // Serial.println("LoRa Sender"); 14 | 15 | if (!LoRa.begin(868E6)) { 16 | // Serial.println("Starting LoRa failed!"); 17 | while (1); 18 | } 19 | } 20 | 21 | void loop() { 22 | // Serial.print("Sending packet: "); 23 | // Serial.println(counter); 24 | 25 | // send packet 26 | LoRa.beginPacket(); 27 | LoRa.print("hello "); 28 | LoRa.print(counter); 29 | LoRa.endPacket(); 30 | delay(3000); 31 | counter++; 32 | } 33 | -------------------------------------------------------------------------------- /FOSDEM2024/Arduino/LoraSenderTest/LoraSenderTest.ino.co2ampel.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschlenker/checkmk-snippets/49abc3923b248651f4cb67b8d346c13f2691d622/FOSDEM2024/Arduino/LoraSenderTest/LoraSenderTest.ino.co2ampel.bin -------------------------------------------------------------------------------- /FOSDEM2024/Arduino/LoraSenderTest/LoraSenderTest.ino.with_bootloader.co2ampel.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschlenker/checkmk-snippets/49abc3923b248651f4cb67b8d346c13f2691d622/FOSDEM2024/Arduino/LoraSenderTest/LoraSenderTest.ino.with_bootloader.co2ampel.bin -------------------------------------------------------------------------------- /FOSDEM2024/MicroPython/discobaby.py: -------------------------------------------------------------------------------- 1 | 2 | import time 3 | import random 4 | from machine import Pin 5 | from neopixel import NeoPixel 6 | 7 | numpix = 192 8 | hexes = 24 9 | 10 | pin = Pin(39, Pin.OUT) 11 | np = NeoPixel(pin, numpix) 12 | colors = [ 13 | (0,255,0), 14 | (192,192,0), 15 | (255,0,0), 16 | (0,255,0) 17 | ] 18 | 19 | def all_red(np): 20 | for n in range(0, 12): 21 | np[n] = (255,0,0) 22 | np.write() 23 | 24 | def all_green(np): 25 | for n in range(0, 12): 26 | np[n] = (0,255,0) 27 | np.write() 28 | 29 | def all_yellow(np): 30 | for n in range(0, 12): 31 | np[n] = (192,192,0) 32 | np.write() 33 | 34 | def cycle(np): 35 | all_off(np) 36 | for n in range(0, 60): 37 | np[n % 12] = (255,255,255) 38 | np[(n + 11) % 12] = (0,0,0) 39 | np.write() 40 | time.sleep(0.5) 41 | 42 | def all_off(np): 43 | for n in range(0, 12): 44 | np[n] = (0,0,0) 45 | np.write() 46 | 47 | def one_on(n): 48 | np[(n + numpix - 1) % numpix] = (0,0,0) 49 | np[n] = (255,255,255) 50 | np.write() 51 | 52 | def random_random(): 53 | randhex = random.randint(0, hexes - 1) 54 | randcolor = random.randint(0, 3) 55 | for n in range(randhex * 8, randhex * 8 + 7): 56 | print(n) 57 | print(randcolor) 58 | print(colors[randcolor]) 59 | np[n] = colors[randcolor] 60 | np.write() 61 | 62 | def switch_color(hex, col): 63 | for n in range(hex * 8, hex * 8 + 7): 64 | np[n] = colors[col] 65 | np.write() 66 | 67 | def colorcycle(color, interval): 68 | print("Color cycle") 69 | for n in range(0, hexes): 70 | switch_color(n, color) 71 | time.sleep(interval) 72 | 73 | def redgreen(interval): 74 | print("Red/green") 75 | for n in range(0, hexes): 76 | if n % 2 == 0: 77 | switch_color(n, 0) 78 | else: 79 | switch_color(n, 2) 80 | time.sleep(interval) 81 | for n in range(0, hexes): 82 | if n % 2 == 0: 83 | switch_color(n, 2) 84 | else: 85 | switch_color(n, 0) 86 | time.sleep(interval) 87 | 88 | for n in range(0, numpix): 89 | print(n) 90 | one_on(n) 91 | time.sleep(0.1) 92 | np[numpix - 1] = (0,0,0) 93 | np.write() 94 | 95 | while True: 96 | redgreen(2.0) 97 | colorcycle(0, 3.0) 98 | time.sleep(15.0) 99 | colorcycle(2, 0.2) 100 | time.sleep(5.0) 101 | colorcycle(1, 1.0) 102 | colorcycle(0, 5.0) 103 | time.sleep(30.0) 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /FOSDEM2024/MicroPython/fulltest.py: -------------------------------------------------------------------------------- 1 | 2 | import time 3 | import random 4 | from machine import Pin 5 | from neopixel import NeoPixel 6 | 7 | numpix = 192 8 | hexes = 24 9 | 10 | pin = Pin(39, Pin.OUT) 11 | np = NeoPixel(pin, numpix) 12 | colors = [ 13 | (0,255,0), 14 | (192,192,0), 15 | (255,0,0), 16 | (0,255,0) 17 | ] 18 | 19 | def all_red(np): 20 | for n in range(0, 12): 21 | np[n] = (255,0,0) 22 | np.write() 23 | 24 | def all_green(np): 25 | for n in range(0, 12): 26 | np[n] = (0,255,0) 27 | np.write() 28 | 29 | def all_yellow(np): 30 | for n in range(0, 12): 31 | np[n] = (192,192,0) 32 | np.write() 33 | 34 | def cycle(np): 35 | all_off(np) 36 | for n in range(0, 60): 37 | np[n % 12] = (255,255,255) 38 | np[(n + 11) % 12] = (0,0,0) 39 | np.write() 40 | time.sleep(0.5) 41 | 42 | def all_off(np): 43 | for n in range(0, 12): 44 | np[n] = (0,0,0) 45 | np.write() 46 | 47 | def one_on(n): 48 | np[(n + numpix - 1) % numpix] = (0,0,0) 49 | np[n] = (255,255,255) 50 | np.write() 51 | 52 | def random_random(): 53 | randhex = random.randint(0, hexes - 1) 54 | randcolor = random.randint(0, 3) 55 | for n in range(randhex * 8, randhex * 8 + 7): 56 | print(n) 57 | print(randcolor) 58 | print(colors[randcolor]) 59 | np[n] = colors[randcolor] 60 | np.write() 61 | 62 | def switch_color(hex, col): 63 | for n in range(hex * 8, hex * 8 + 7): 64 | np[n] = colors[col] 65 | np.write() 66 | 67 | for n in range(0, numpix): 68 | print(n) 69 | one_on(n) 70 | time.sleep(0.1) 71 | np[numpix - 1] = (0,0,0) 72 | np.write() 73 | 74 | while True: 75 | i = input() 76 | h = 0 77 | c = 0 78 | # try: 79 | toks = i.split() 80 | h = int(toks[0]) 81 | c = int(toks[1]) 82 | switch_color(h, c) 83 | #except: 84 | #print("Could not parse/switch line: " + i) 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /FOSDEM2024/Python/dumplora.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import time 4 | import json 5 | import serial 6 | import sys 7 | import getopt 8 | 9 | co2 = 0 10 | temp = 0 11 | humidity = 0 12 | lighting = 0 13 | lastwrite = 0 14 | 15 | opts, args = getopt.getopt(sys.argv[1:],"p:o:s:i:h") 16 | for opt, arg in opts: 17 | if opt == '-s': 18 | port = arg 19 | elif opt == '-i': 20 | interval = int(arg) 21 | elif opt == '-h': 22 | print(help) 23 | sys.exit() 24 | 25 | ser = serial.Serial(port, 115200, timeout=3) 26 | 27 | while True: 28 | data = ser.readline() 29 | j = json.loads("{}") 30 | try: 31 | if data.decode('utf-8').strip() and json.loads(data.decode('utf-8').strip()): 32 | print(data) 33 | j = json.loads(data.decode('utf-8').strip()) 34 | h = j['n'] 35 | outfile = "/tmp/" + h + ".txt" 36 | o = open(outfile, "w") 37 | o.write("<<>>\n") 38 | o.write("Version: 2.0.0p1\n") 39 | o.write("AgentOS: arduino\n") 40 | o.write("Hostname: " + h + "\n") 41 | o.write("<<>>\n") 42 | o.write("co2 " + str(j['c']) + "\n") 43 | o.write("temp " + str(j['t']) + "\n") 44 | o.write("humidity " + str(j['h']) + "\n") 45 | o.write("lighting " + str(j['l']) + "\n") 46 | o.close() 47 | except: 48 | print("Ooops, malformed data!") 49 | 50 | -------------------------------------------------------------------------------- /FOSDEM2024/Python/status2ws.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import time 4 | import json 5 | import serial 6 | import sys 7 | import getopt 8 | import requests 9 | import pprint 10 | import serial 11 | 12 | HOST_NAME = "localhost" 13 | SITE_NAME = "cmktest" 14 | API_URL = f"http://{HOST_NAME}/{SITE_NAME}/check_mk/api/1.0" 15 | 16 | USERNAME = "cmkadmin" 17 | PASSWORD = "test123" 18 | 19 | hexes = { 20 | "co2_1" : { 21 | "Check_MK" : 0, 22 | "CO2 board co2" : 11, 23 | "CO2 board temp" : 12, 24 | "CO2 board humidity" : 23, 25 | "CO2 board lighting" : 6 26 | 27 | }, 28 | "co2_2" : { 29 | "Check_MK" : 1, 30 | "CO2 board co2" : 10, 31 | "CO2 board temp" : 13, 32 | "CO2 board humidity" : 22, 33 | "CO2 board lighting" : 7 34 | }, 35 | "co2_3" : { 36 | "Check_MK" : 2, 37 | "CO2 board co2" : 9, 38 | "CO2 board temp" : 14, 39 | "CO2 board humidity" : 21, 40 | "CO2 board lighting" : 18 41 | }, 42 | "co2_4" : { 43 | "Check_MK" : 3, 44 | "CO2 board co2" : 8, 45 | "CO2 board temp" : 15, 46 | "CO2 board humidity" : 20, 47 | "CO2 board lighting" : 19 48 | }, 49 | } 50 | 51 | ser = serial.Serial("/dev/ttyACM1", 115200) 52 | 53 | session = requests.session() 54 | session.headers['Authorization'] = f"Bearer {USERNAME} {PASSWORD}" 55 | session.headers['Accept'] = 'application/json' 56 | 57 | while True: 58 | for k in hexes: 59 | resp = session.get( 60 | f"{API_URL}/objects/host/{k}/collections/services", 61 | params={ 62 | "query": '{"op": "=", "left": "host_name", "right": "' + k + '"}', 63 | "columns": ['host_name', 'description', 'state' ], 64 | }, 65 | ) 66 | print(resp.status_code) 67 | if resp.status_code == 200: 68 | # pprint.pprint(resp.json()) 69 | j = resp.json() 70 | for s in j['value']: 71 | print(s['extensions']['description'] + " has value " + str(s['extensions']['state'])) 72 | o = (str(hexes[k][s['extensions']['description']]) + " " + str(s['extensions']['state']) + "\r").encode("utf-8") 73 | # print(o) 74 | ser.write(o) 75 | time.sleep(15) 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /FOSDEM2024/README.md: -------------------------------------------------------------------------------- 1 | # LORA enabled CO2 monitoring at FOSDEM 24 and Checkmk Conference #10 2 | 3 | After having experienced trouble with WiFi enabled CO2 sensors in environments where the 2.4GHz band isvery congested, we opted for a different wireless standard this time. 4 | Since simplicity, robustness and ease of setup were the main goals, we opted for LoRa this time. 5 | Readily available CO2 sensor boards in various configurations are available from [Watterott Electronic](https://watterott.com/). 6 | To keep it simple, we skipped the LoRaWAN part and configured for a simple broadcast without ACK. 7 | 8 | To display the readings, not only Checkmk was used, but also 3D printed honeycombs that used WS2812 rings to display red, yellow, green. 9 | This involved a script querying the Checkmk REST API and writing to a serial interface. 10 | On the serial interface an ESP32 running Micropython switched colors according to serial data received. 11 | 12 | ## Components used 13 | 14 | ### Arduino for the sending CO2 sensor boards 15 | 16 | We added a LoRa broadcast to the demo firmware from Watterott. 17 | Since it would not compile without the LoRa library (or rather big changes to the head of the example), we will not file a pull request for now. 18 | The data broadcasted is one JSON string. This is a waste of bandwith when configured for lower data rates, for our setup it was sufficient. 19 | [The Arduino code is available here.](./Arduino/CO2-Ampel/examples/CO2-Ampel/CO2-Ampel.ino) 20 | 21 | ### Arduino for the receiver boards 22 | 23 | To receive, we configured one board to receive packets and just dump every packet to the serial interface. 24 | We initially wanted to build a receiver just out of a microcontroller and a LoRa Transceiver, but eventually perpetuated the setup when one NDIR sensor broke. 25 | [The Arduino code is available here.](./Arduino/LoraReceiverTest/LoraReceiverTest.ino) 26 | 27 | ### Python to convert received data to Checkmk agent output 28 | 29 | A [Python script](./Python/dumplora.py) was used to listen on the serial port the LoRa receiver was connected to. 30 | After every line it was checked whether this was a valid JSON object and then converted to Checkmk agent output in the format used for the [Watterott CO2 plugin for Checkmk](https://exchange.checkmk.com/p/watterott-co2-ampel). 31 | To use the data in the monitoring, it was sufficient to read the agent output file using a data source call: 32 | 33 | cat /tmp/$HOSTNAME$.txt 34 | 35 | ### Python to query the Checkmk REST API 36 | 37 | Another [Python script](./Python/status2ws.py) is used to query the Checkmk REST API and then write to a serial interface which honeycomb should display which color. 38 | 39 | ### Micropython on ESP to switch WS2812 40 | 41 | A [MicroPython script](./MicroPython/fulltest.py) on ESP32 receives this data, interprets it and switches the WS2812 accordingly. 42 | 43 | ## 3D printed hexagons 44 | 45 | Hexagons with 100mm outer diameter were made with a 3D printer. To keep printing time shorter, the hexagons were made to fit to 80mm plastic tubing. 46 | The folder [3D_print](./3D_print) contains the CAD files and STL. 47 | See the [images](./images) on how the parts are assembled. 48 | -------------------------------------------------------------------------------- /FOSDEM2024/images/IMG_20240615_150747.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschlenker/checkmk-snippets/49abc3923b248651f4cb67b8d346c13f2691d622/FOSDEM2024/images/IMG_20240615_150747.jpg -------------------------------------------------------------------------------- /FOSDEM2024/images/IMG_20240615_150804.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschlenker/checkmk-snippets/49abc3923b248651f4cb67b8d346c13f2691d622/FOSDEM2024/images/IMG_20240615_150804.jpg -------------------------------------------------------------------------------- /FOSDEM2024/images/IMG_20240615_150829.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschlenker/checkmk-snippets/49abc3923b248651f4cb67b8d346c13f2691d622/FOSDEM2024/images/IMG_20240615_150829.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Snippets and examples for Checkmk 2 | 3 | Just a collection of snippets for Checkmk. These are either result of my paid work for Checkmk GmbH or part of my work for servers I maintain. Or they might be made just out of curiosity. 4 | 5 | Either way: Some of the snippets might find their way to other places like Checkmk documentation, in these cases, I'll mark appropriately. 6 | -------------------------------------------------------------------------------- /bootstrap/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschlenker/checkmk-snippets/49abc3923b248651f4cb67b8d346c13f2691d622/bootstrap/README.md -------------------------------------------------------------------------------- /bootstrap/dummybridge.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # (c) 2024 Mattias Schlenker for Checkmk GmbH 4 | # License: GPL v2 5 | 6 | # Specify the virtual network to use, can be overwritten using a file 7 | # bridgeconfig.cfg that must exist in the current working directory: 8 | VIP="10.23.11.1" 9 | MASK="24" 10 | VNET="10.23.11.0/24" 11 | DUMMY="vmdummy0" 12 | BRIDGE="vmbridge0" 13 | TAPRANGE="0 9" 14 | 15 | # The config file also can contain entries for WIFI/ETH interfaces. 16 | # Separate them using whitespace 17 | # PHYIFACES="wlp114s0 enp0s31f6 enbx00decafbad00" 18 | # 19 | # The tap user should be read from the sudo user, but can be specified: 20 | # TAPUSER="hhirsch" 21 | PHYIFACES="" 22 | TAPUSER="" 23 | 24 | if [ -f ./bridgeconfig.cfg ] ; then 25 | echo "Reading config from file: `pwd`/bridgeconfig.cfg:" 26 | cat ./bridgeconfig.cfg 27 | . ./bridgeconfig.cfg 28 | fi 29 | 30 | # Find settings that were not specified via config: 31 | if [ -z "$TAPUSER" ] ; then 32 | TAPUSER="$SUDO_USER" 33 | fi 34 | if [ -z "$PHYIFACES" ] ; then 35 | PHYIFACES="` ip link ls | grep -v '^\s' | awk -F ': ' '{print $2}' | grep -e 'wl[opx]' `" 36 | PHYIFACES="${PHYIFACES} ` ip link ls | grep -v '^\s' | awk -F ': ' '{print $2}' | grep -e 'en[opx]' `" 37 | fi 38 | # Exit if configuration is invalid 39 | if [ -z "$TAPUSER" ] ; then 40 | echo "Please run this script either using sudo or specify the TAPUSER via ./bridgeconfig.cfg." 41 | exit 1 42 | fi 43 | if [ -z "$PHYIFACES" ] ; then 44 | echo "Please use ./bridgeconfig.cfg to specify the outgoing interfaces via PHYIFACES." 45 | exit 1 46 | fi 47 | if [ "$UID" -gt 0 ] ; then 48 | echo "Please run as root. Exiting." 49 | exit 1 50 | fi 51 | 52 | if ip link show $DUMMY > /dev/null 2>&1 ; then 53 | echo "Interface $DUMMY exists. Skipping..." 54 | else 55 | ip link add $DUMMY type dummy 56 | echo "Checking $DUMMY:" 57 | ip link show $DUMMY 58 | fi 59 | 60 | if ip link show $BRIDGE > /dev/null 2>&1 ; then 61 | echo "Interface $BRIDGE exists. Skipping..." 62 | else 63 | ip link add name $BRIDGE type bridge 64 | ip link set dev $BRIDGE up 65 | ip link set dev $DUMMY master $BRIDGE 66 | echo "Checking $BRIDGE:" 67 | ip link show $BRIDGE 68 | fi 69 | 70 | for n in `seq $TAPRANGE` ; do 71 | if ip link show vmtap${n} > /dev/null 2>&1 ; then 72 | echo "Interface vmtap${n} exists. If you want to recreate, tear down first with:" 73 | echo "ip tuntap del dev vmtap${n} mode tap" 74 | else 75 | echo "Creating tap interface: vmtap${n} owned by $TAPUSER" 76 | ip tuntap add dev vmtap${n} mode tap user $TAPUSER group $TAPUSER 77 | ip link set vmtap${n} master $BRIDGE; 78 | ip link set dev vmtap${n} up 79 | fi 80 | done 81 | 82 | ip a add ${VIP}/${MASK} dev $BRIDGE > /dev/null 2>&1 83 | ip route add $VNET via $VIP > /dev/null 2>&1 84 | 85 | # Enable IPv4 forwarding 86 | sysctl -w net.ipv4.ip_forward=1 87 | 88 | # This is not nice, it forwards all traffic from the virtual net to each device... 89 | # The result is having test VMs spamming the VPN. 90 | 91 | # iptables -A FORWARD -j ACCEPT 92 | # iptables -t nat -s $VNET -A POSTROUTING -j MASQUERADE 93 | 94 | # This is better, forward only to physical interfaces specified above: 95 | for iface in $PHYIFACES ; do 96 | iptables -A FORWARD -i $BRIDGE -o $iface -j ACCEPT 97 | iptables -A FORWARD -i $iface -o $BRIDGE -m state --state ESTABLISHED,RELATED -j ACCEPT 98 | iptables -t nat -A POSTROUTING -s $VNET -o $iface -j MASQUERADE 99 | done 100 | 101 | # Check the state of the DHCP server: 102 | 103 | DHCPERR=0 104 | if grep $BRIDGE /etc/default/isc-dhcp-server > /dev/null 2>&1 ; then 105 | echo "Found bridge in /etc/default/isc-dhcp-server" 106 | else 107 | DHCPERR=$(( $DHCPERR + 1 )) 108 | fi 109 | if grep "routers $VIP" /etc/dhcp/dhcpd.conf > /dev/null 2>&1 ; then 110 | echo "Found gateway IP in /etc/dhcp/dhcpd.conf" 111 | else 112 | DHCPERR=$(( $DHCPERR + 1 )) 113 | fi 114 | 115 | if [ "$DHCPERR" -lt 1 ] ; then 116 | echo "Restarting DHCP server..." 117 | systemctl restart isc-dhcp-server 118 | echo 'Have fun!' 119 | exit 0 120 | fi 121 | 122 | # If we have an incomplete DHCP configuration, give the user some hints on how to set up: 123 | echo "" 124 | echo "The virtual network is running, but no DHCP is available yet. Thus you might" 125 | echo "use a static configuration for your clients with these parameters:" 126 | echo "" 127 | echo "Network: $VNET" 128 | echo "Gateway: $VIP" 129 | echo "DNS: 1.1.1.1" 130 | echo "" 131 | echo "In case you want to run a DHCP server (recommended), install isc-dhcp-server" 132 | echo "and use this configuration:" 133 | echo "" 134 | echo "# /etc/dhcp/dhcpd.conf" 135 | 136 | SUBNET=` echo $VNET | awk -F '/' '{print $1}'` 137 | NETPREF=` echo $SUBNET | awk -F '.' '{print $1"."$2"."$3}'` 138 | 139 | cat << EOF 140 | option domain-name "checkmk.example"; 141 | option domain-name-servers 1.1.1.1; 142 | default-lease-time 600; 143 | max-lease-time 7200; 144 | ddns-update-style none; 145 | subnet $NETPREF.0 netmask 255.255.255.0 { 146 | range $NETPREF.100 $NETPREF.199; 147 | option routers $VIP; 148 | } 149 | 150 | EOF 151 | 152 | echo "# /etc/default/isc-dhcp-server" 153 | 154 | cat << EOF 155 | INTERFACESv4="$BRIDGE" 156 | INTERFACESv6="" 157 | 158 | EOF 159 | 160 | echo "Just restart isc-dhcp-server after changing the config, next time this will be" 161 | echo "done automatically." 162 | echo "" 163 | echo 'Have fun!' 164 | -------------------------------------------------------------------------------- /bootstrap/quickdebuntu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script for debootstrapping and running an Ubuntu, choose any recent Ubuntu. 4 | # If installing a newer Ubuntu than the one you are running this script on, 5 | # make sure you have installed the most recent debootstrap! 6 | # 7 | # (c) Mattias Schlenker for tribe29 GmbH 8 | # 9 | # License: Three clause BSD 10 | 11 | # For building, please adjust! 12 | 13 | TARGETDIR="$1" 14 | # Only specify one of ! Precedence: Ubuntu, Debian, Devuan 15 | UBUEDITION="jammy" # jammy: 22.04, impish: 21.10, focal: 20.04 16 | DEBEDITION="bookworm" # bookworm: 12, bullseye: 11.x Takes precedence over Devuan 17 | DEVEDITION="daedalus" # daedalus 5.0 = 12.x, Devuan is Debian without systemd 18 | # Make sure you have devootstrap scripts or install Devuan debootstrap 19 | # http://deb.devuan.org/devuan/pool/main/d/debootstrap/ 20 | SYSSIZE=32 # Size of the system partition GB 21 | SWAPSIZE=3 # Size of swap GB 22 | # BOOTSIZE=3 # Keep a small boot partition, 3GB is sufficient for kernel, initrd and modules (twice) 23 | TMPSIZE=512 # MB Create a small tmpfs on /tmp, this only affects /etc/fstab, 0 to disable 24 | ARCH=amd64 25 | ROOTFS=btrfs # You might choose ext4 or zfs (haven't tried), btrfs uses snapshots 26 | SSHKEYS="/home/${SUDO_USER}/.ssh/id_ecdsa.pub /home/${SUDO_USER}/.ssh/id_ed25519.pub" 27 | NAMESERVER=8.8.8.8 # Might or might not be overwritten later by DHCP. 28 | HOSTNAME="throwawaybian" 29 | EXTRADEBS="" 30 | ADDUSER="" # "karlheinz" If non-empty a user will be added. This means interaction! 31 | ROOTPASS=0 # Set to 1 to prompt for a root password. This means interaction! 32 | PKGCACHE="" # /data/VM/debcache" # Set to nonzero length directory name to enable caching of debs 33 | LINUXIMAGE="" # Set for example to linux-virtual on Ubuntu to install a leaner kernel 34 | UBUSERVER="http://archive.ubuntu.com/ubuntu" # You might change to local mirror, but 35 | DEBSERVER="http://deb.debian.org/debian" # this is less relevant when using caching! 36 | DEVSERVER="http://deb.devuan.org/merged" 37 | 38 | # For running, please adjust! 39 | 40 | CPUS=2 41 | MEM=2048 42 | VNC=":23" 43 | DAEMONIZE="-daemonize" # set to empty string to run in foreground 44 | EXTRAS="" # add additional CLI parameters 45 | # This redirects port 8000 on the local machine to 80 on the virtualized Ubuntu 46 | # and port 2222 to 22 on the Ubuntu. This is often sufficient for development: 47 | NET="-net nic,model=e1000 -net user,hostfwd=tcp::8000-:80,hostfwd=tcp::2222-:22" 48 | # This uses a real tun/tap bridge, use for stationary machines that should be 49 | # exposed, if using Mattias' bridge script you have vmtap0 to vmtap9 available: 50 | # NET="-device virtio-net-pci,netdev=network3,mac=00:16:17:12:23:11 -netdev tap,id=network3,ifname=vmtap3,script=no,downscript=no" 51 | 52 | # network3 in both parameters is just an identifier to make qemu know, both 53 | # parameters belong together. 54 | # 55 | # tap3 is the name of the tap device the interface is bonded to. This has to 56 | # be unique for each virtual machine. 57 | # 58 | # The MAC address also has to be unique for each virtual machine! 59 | # 60 | # You might just snip the lines above and copy to the target dir than these 61 | # lines will be sourced, so you do not have to modify this script. Just run: 62 | # 63 | # vim.tiny /path/to/installation/config.sh 64 | # chmod a+x /path/to/installation/config.sh 65 | # 66 | # quickdebuntu.sh /path/to/installation 67 | # 68 | ####################### SNIP HERE ############################################ 69 | 70 | if [ -z "$TARGETDIR" ] ; then 71 | echo "Please specify a target directory." 72 | exit 1 73 | fi 74 | 75 | if [ "$UID" -gt 0 ] ; then 76 | echo "Please run as root." 77 | exit 1 78 | fi 79 | 80 | CFG="config.sh" 81 | if [ -f "${TARGETDIR}" ] ; then 82 | # A file is specified, assume that this is a config file in the folder containing the VM" 83 | echo "File instead of directory given, splitting..." 84 | chmod +x "${TARGETDIR}" 85 | CFG=` basename "${TARGETDIR}" ` 86 | TARGETDIR=` dirname "${TARGETDIR}" ` 87 | echo "cfg file: $CFG" 88 | echo "cfg dir: $TARGETDIR" 89 | fi 90 | 91 | if [ -x "${TARGETDIR}/${CFG}" ] ; then 92 | echo "Found config: ${TARGETDIR}/${CFG}, sourcing it..." 93 | . "${TARGETDIR}/${CFG}" 94 | else 95 | echo "Creating config: ${TARGETDIR}/config.sh..." 96 | lines=`grep -n 'SNIP HERE' "$0" | head -n 1 | awk -F ':' '{print $1}' ` 97 | mkdir -p "${TARGETDIR}" 98 | head -n $lines "$0" | sed 's/^TARGETDIR/# TARGETDIR/g' > "${TARGETDIR}/config.sh" 99 | chmod +x "${TARGETDIR}/config.sh" 100 | fi 101 | 102 | if [ -n "$PKGCACHE" ]; then 103 | if [ -n "$UBUEDITION" ] ; then 104 | mkdir -p "${PKGCACHE}/ubuntu/archives" 105 | elif [ -n "$DEBEDITION" ] ; then 106 | mkdir -p "${PKGCACHE}/debian/archives" 107 | else 108 | # Well that's not perfect, everything above the base 109 | # system should be taken from matching Debian! 110 | mkdir -p "${PKGCACHE}/devuan/archives" 111 | fi 112 | fi 113 | 114 | DISKSIZE=$(( $SYSSIZE + $SWAPSIZE )) 115 | freeloop="" 116 | 117 | # If a file .bootstrap.success is present, assume installation was OK. 118 | # In this case do not check for tools: 119 | neededtools="parted dmsetup kpartx debootstrap mkfs.btrfs qemu-system-x86_64" 120 | if [ -f "${TARGETDIR}/.bootstrap.success" ] ; then 121 | echo "Found ${TARGETDIR}/.bootstrap.success, skipping checks" 122 | else 123 | for tool in $neededtools ; do 124 | if which $tool > /dev/null ; then 125 | echo "Found: $tool" 126 | else 127 | echo "Missing: $tool, please install $neededtools" 128 | exit 1 129 | fi 130 | done 131 | for key in $SSHKEYS ; do 132 | if [ '!' -f "$key" ] ; then 133 | echo "Missing SSH key $key, you would not be able to login." 134 | exit 1 135 | fi 136 | done 137 | # Check whether using a fixed path really suits all Debian derivatives? 138 | CODENAME="" 139 | if [ -n "$UBUEDITION" ] ; then 140 | CODENAME="$UBUEDITION" 141 | elif [ -n "$DEBEDITION" ] ; then 142 | CODENAME="$DEBEDITION" 143 | else 144 | CODENAME="$DEVEDITION" 145 | fi 146 | if [ -z "$CODENAME" ] ; then 147 | echo "Please specify either UBUEDITION, DEBEDITION or DEVEDITION. Exiting." 148 | exit 1 149 | fi 150 | if [ '!' -f "/usr/share/debootstrap/scripts/${CODENAME}" ] ; then 151 | echo "Bootstrap script missing for ${CODENAME}. Exiting." 152 | exit 1 153 | fi 154 | fi 155 | # Create a hard disk and partition it: 156 | mkdir -p "${TARGETDIR}" 157 | if [ -f "${TARGETDIR}/disk.img" ] ; then 158 | echo "Disk exists, skipping." 159 | else 160 | dd if=/dev/zero bs=1M of="${TARGETDIR}/disk.img" count=1 seek=$(( ${DISKSIZE} * 1024 - 1 )) 161 | freeloop=` losetup -f ` 162 | losetup $freeloop "${TARGETDIR}/disk.img" 163 | # Partition the disk 164 | parted -s $freeloop mklabel msdos 165 | parted -s $freeloop unit B mkpart primary ext4 $(( 1024 ** 2 )) $(( 1024 ** 3 * $SYSSIZE - 1 )) 166 | # parted -s $freeloop unit B mkpart primary ext4 $(( 1024 ** 3 * $BOOTSIZE )) $(( 1024 ** 3 * ( $BOOTSIZE + $SWAPSIZE ) - 1 )) 167 | parted -s $freeloop unit B mkpart primary ext4 $(( 1024 ** 3 * $SYSSIZE )) 100% 168 | parted -s $freeloop unit B set 1 boot on 169 | parted -s $freeloop unit B print 170 | fi 171 | 172 | # Mount and debootstrap: 173 | 174 | if [ -f "${TARGETDIR}/.bootstrap.success" ] ; then 175 | echo "Already bootstrapped, skipping." 176 | else 177 | if [ -z "$freeloop" ] ; then 178 | freeloop=` losetup -f ` 179 | losetup $freeloop "${TARGETDIR}/disk.img" 180 | fi 181 | sync 182 | sleep 5 183 | kpartx -a $freeloop 184 | mkdir -p "${TARGETDIR}/.target" 185 | # mkfs.ext4 /dev/mapper/${freeloop#/dev/}p1 186 | mkfs.${ROOTFS} /dev/mapper/${freeloop#/dev/}p1 187 | # When using btrfs create a subvolume _install and use as default to make versioning easier 188 | MOUNTOPTS="defaults" 189 | case ${ROOTFS} in 190 | btrfs) 191 | mount -o rw /dev/mapper/${freeloop#/dev/}p1 "${TARGETDIR}/.target" 192 | btrfs subvolume create "${TARGETDIR}/.target/_install" 193 | btrfs subvolume set-default "${TARGETDIR}/.target/_install" 194 | umount /dev/mapper/${freeloop#/dev/}p1 195 | MOUNTOPTS='subvol=_install' 196 | ;; 197 | esac 198 | mkswap /dev/mapper/${freeloop#/dev/}p2 199 | mount -o rw,"${MOUNTOPTS}" /dev/mapper/${freeloop#/dev/}p1 "${TARGETDIR}/.target" 200 | mkdir -p "${TARGETDIR}/.target/boot" 201 | # mount -o rw /dev/mapper/${freeloop#/dev/}p1 "${TARGETDIR}/.target/boot" 202 | # mkdir -p "${TARGETDIR}/.target/boot/modules" 203 | # mkdir -p "${TARGETDIR}/.target/boot/firmware" 204 | # mkdir -p "${TARGETDIR}/.target/lib" 205 | # ln -s /boot/modules "${TARGETDIR}/.target/lib/modules" 206 | # ln -s /boot/firmware "${TARGETDIR}/.target/lib/firmware" 207 | # This is the installation! 208 | archivedir="" 209 | if [ -n "$PKGCACHE" ]; then 210 | if [ -n "$UBUEDITION" ] ; then 211 | archivedir="${PKGCACHE}/ubuntu/archives" 212 | elif [ -n "$DEBEDITION" ] ; then 213 | archivedir="${PKGCACHE}/debian/archives" 214 | else 215 | archivedir="${PKGCACHE}/devuan/archives" 216 | fi 217 | fi 218 | mkdir -p "${TARGETDIR}/.target"/var/cache/apt/archives 219 | if [ -n "$PKGCACHE" ]; then 220 | mount --bind "$archivedir" "${TARGETDIR}/.target"/var/cache/apt/archives 221 | else 222 | mount -t tmpfs -o size=4G,mode=0755 tmpfs "${TARGETDIR}/.target"/var/cache/apt/archives 223 | fi 224 | if [ -n "$UBUEDITION" ] ; then 225 | debootstrap --arch $ARCH $UBUEDITION "${TARGETDIR}/.target" $UBUSERVER 226 | elif [ -n "$DEBEDITION" ] ; then 227 | debootstrap --arch $ARCH $DEBEDITION "${TARGETDIR}/.target" $DEBSERVER 228 | else 229 | debootstrap --arch $ARCH $DEVEDITION "${TARGETDIR}/.target" $DEVSERVER 230 | fi 231 | mount -t proc none "${TARGETDIR}/.target"/proc 232 | mount --bind /sys "${TARGETDIR}/.target"/sys 233 | mount --bind /dev "${TARGETDIR}/.target"/dev 234 | mount -t devpts none "${TARGETDIR}/.target"/dev/pts 235 | echo 'en_US.UTF-8 UTF-8' > "${TARGETDIR}/.target"/etc/locale.gen 236 | mkdir -p "${TARGETDIR}/.target"/etc/initramfs-tools 237 | echo btrfs >> "${TARGETDIR}/.target"/etc/initramfs-tools/modules 238 | echo ext4 >> "${TARGETDIR}/.target"/etc/initramfs-tools/modules 239 | chroot "${TARGETDIR}/.target" locale-gen 240 | chroot "${TARGETDIR}/.target" shadowconfig on 241 | if [ -n "$UBUEDITION" ] ; then 242 | 243 | cat > "${TARGETDIR}/.target"/etc/apt/sources.list << EOF 244 | 245 | deb http://de.archive.ubuntu.com/ubuntu/ ${UBUEDITION} main restricted 246 | deb-src http://de.archive.ubuntu.com/ubuntu/ ${UBUEDITION} main restricted 247 | deb http://de.archive.ubuntu.com/ubuntu/ ${UBUEDITION}-updates main restricted 248 | deb-src http://de.archive.ubuntu.com/ubuntu/ ${UBUEDITION}-updates main restricted 249 | deb http://de.archive.ubuntu.com/ubuntu/ ${UBUEDITION} universe 250 | deb-src http://de.archive.ubuntu.com/ubuntu/ ${UBUEDITION} universe 251 | deb http://de.archive.ubuntu.com/ubuntu/ ${UBUEDITION}-updates universe 252 | deb-src http://de.archive.ubuntu.com/ubuntu/ ${UBUEDITION}-updates universe 253 | deb http://de.archive.ubuntu.com/ubuntu/ ${UBUEDITION} multiverse 254 | deb-src http://de.archive.ubuntu.com/ubuntu/ ${UBUEDITION} multiverse 255 | deb http://de.archive.ubuntu.com/ubuntu/ ${UBUEDITION}-updates multiverse 256 | deb-src http://de.archive.ubuntu.com/ubuntu/ ${UBUEDITION}-updates multiverse 257 | deb http://de.archive.ubuntu.com/ubuntu/ ${UBUEDITION}-backports main restricted universe multiverse 258 | deb-src http://de.archive.ubuntu.com/ubuntu/ ${UBUEDITION}-backports main restricted universe multiverse 259 | deb http://security.ubuntu.com/ubuntu ${UBUEDITION}-security main restricted 260 | deb-src http://security.ubuntu.com/ubuntu ${UBUEDITION}-security main restricted 261 | deb http://security.ubuntu.com/ubuntu ${UBUEDITION}-security universe 262 | deb-src http://security.ubuntu.com/ubuntu ${UBUEDITION}-security universe 263 | deb http://security.ubuntu.com/ubuntu ${UBUEDITION}-security multiverse 264 | deb-src http://security.ubuntu.com/ubuntu ${UBUEDITION}-security multiverse 265 | 266 | EOF 267 | 268 | elif [ -n "$DEBEDITION" ] ; then 269 | 270 | cat > "${TARGETDIR}/.target"/etc/apt/sources.list << EOF 271 | 272 | deb http://deb.debian.org/debian/ ${DEBEDITION} main contrib non-free 273 | deb https://security.debian.org/debian-security ${DEBEDITION}-security main contrib non-free 274 | deb http://deb.debian.org/debian/ ${DEBEDITION}-updates main contrib non-free 275 | deb http://deb.debian.org/debian ${DEBEDITION}-proposed-updates main contrib non-free 276 | deb http://deb.debian.org/debian-security/ ${DEBEDITION}-security main contrib non-free 277 | # deb http://deb.debian.org/debian/ ${DEBEDITION}-backports main contrib non-free 278 | 279 | EOF 280 | 281 | else 282 | cat > "${TARGETDIR}/.target"/etc/apt/sources.list << EOF 283 | 284 | deb http://deb.devuan.org/merged ${DEVEDITION} main 285 | deb http://deb.devuan.org/merged ${DEVEDITION}-updates main 286 | deb http://deb.devuan.org/merged ${DEVEDITION}-security main 287 | 288 | EOF 289 | 290 | fi 291 | # Devuan users shall manually adjust their sources.list, since they mix in matching Debian! 292 | 293 | chroot "${TARGETDIR}/.target" apt-get -y install ca-certificates 294 | chroot "${TARGETDIR}/.target" apt-get -y update 295 | [ -z "$LINUXIMAGE" ] && LINUXIMAGE=linux-image-generic 296 | chroot "${TARGETDIR}/.target" apt-get -y install screen $LINUXIMAGE openssh-server \ 297 | rsync btrfs-progs openntpd ifupdown net-tools locales grub-pc os-prober 298 | chroot "${TARGETDIR}/.target" apt-get -y install grub-gfxpayload-lists 299 | chroot "${TARGETDIR}/.target" apt-get -y dist-upgrade 300 | extlinux -i "${TARGETDIR}/.target/boot" 301 | if [ -z "$UBUEDITION" ] ; then 302 | kernel=` ls "${TARGETDIR}/.target/boot/" | grep vmlinuz- | tail -n 1 ` 303 | initrd=` ls "${TARGETDIR}/.target/boot/" | grep initrd.img- | tail -n 1 ` 304 | ln -s $kernel "${TARGETDIR}/.target/boot/vmlinuz" 305 | ln -s $initrd "${TARGETDIR}/.target/boot/initrd.img" 306 | chroot "${TARGETDIR}/.target" locale-gen 307 | fi 308 | for d in $EXTRADEBS ; do 309 | chroot "${TARGETDIR}/.target" apt-get -y install $d 310 | done 311 | echo 'GRUB_CMDLINE_LINUX_DEFAULT="net.ifnames=0"' >> "${TARGETDIR}/.target/etc/default/grub" 312 | chroot "${TARGETDIR}/.target" update-grub 313 | chroot "${TARGETDIR}/.target" grub-install --recheck --target=i386-pc --boot-directory=/boot $freeloop 314 | rm "${TARGETDIR}/.target"/etc/resolv.conf 315 | echo "nameserver $NAMESERVER" > "${TARGETDIR}/.target"/etc/resolv.conf 316 | echo "$HOSTNAME" > "${TARGETDIR}/.target"/etc/hostname 317 | # echo btrfs >> "${TARGETDIR}/.target"/etc/initramfs-tools/modules # Brauchen wir das? 318 | mkdir -m 0600 "${TARGETDIR}/.target/root/.ssh" 319 | for key in $SSHKEYS ; do 320 | [ -f "$key" ] && cat "$key" >> "${TARGETDIR}/.target/root/.ssh/authorized_keys" 321 | done 322 | #eval ` blkid -o udev /dev/mapper/${freeloop#/dev/}p1 ` 323 | #UUID_BOOT=$ID_FS_UUID 324 | eval ` blkid -o udev /dev/mapper/${freeloop#/dev/}p2 ` 325 | UUID_SWAP=$ID_FS_UUID 326 | eval ` blkid -o udev /dev/mapper/${freeloop#/dev/}p1 ` 327 | UUID_ROOT=$ID_FS_UUID 328 | cat > "${TARGETDIR}/.target"/etc/fstab << EOF 329 | # 330 | UUID=${UUID_ROOT} / ${ROOTFS} ${MOUNTOPTS} 0 1 331 | UUID=${UUID_SWAP} none swap sw 0 0 332 | 333 | EOF 334 | 335 | if [ "$TMPSIZE" -gt 0 ] ; then 336 | echo "tmpfs /tmp tmpfs size=${TMPSIZE}M 0 0" >> "${TARGETDIR}/.target"/etc/fstab 337 | fi 338 | 339 | cat > "${TARGETDIR}/.target"/etc/network/interfaces << EOF 340 | source /etc/network/interfaces.d/* 341 | 342 | # The loopback network interface 343 | auto lo 344 | iface lo inet loopback 345 | 346 | # The primary network interface 347 | # allow-hotplug eth0 348 | auto eth0 349 | iface eth0 inet dhcp 350 | 351 | EOF 352 | 353 | # dd if="${TARGETDIR}/.target"/usr/lib/EXTLINUX/mbr.bin of=${freeloop} count=1 bs=448 # max size of an MBR 354 | 355 | #cat > "${TARGETDIR}/.target"/boot/extlinux.conf << EOF 356 | # No frills bootloader config for extlinux/syslinux 357 | #DEFAULT ubuntu 358 | #TIMEOUT 50 359 | #PROMPT 1 360 | # 361 | #LABEL ubuntu 362 | # KERNEL /vmlinuz 363 | # APPEND initrd=/initrd.img root=/dev/vda3 ro nosplash rootflags=${MOUNTOPTS} net.ifnames=0 biosdevname=0 364 | # 365 | #EOF 366 | 367 | # Tunneled devices are not seen from the outside until at least one outgoing 368 | # packet has occured, so just ping the nameservers to make sure, hosts 369 | # with static address configuration are seen from the outside. 370 | 371 | cat > "${TARGETDIR}/.target"/etc/rc.local << EOF 372 | #!/bin/bash 373 | 374 | ping -c 1 $NAMESERVER 375 | exit 0 376 | 377 | EOF 378 | 379 | chmod 0755 "${TARGETDIR}/.target"/etc/rc.local 380 | if [ -n "$ADDUSER" ] ; then 381 | echo "Adding user $ADDUSER" 382 | chroot "${TARGETDIR}/.target" adduser "$ADDUSER" 383 | fi 384 | if [ "$ROOTPASS" -gt 0 ] ; then 385 | echo "Adding a root password for console login" 386 | chroot "${TARGETDIR}/.target" passwd 387 | fi 388 | for d in dev/pts dev sys proc boot var/cache/apt/archives ; do umount -f "${TARGETDIR}/.target"/$d ; done 389 | umount "${TARGETDIR}/.target" 390 | # dmsetup remove /dev/mapper/${freeloop#/dev/}p3 391 | dmsetup remove /dev/mapper/${freeloop#/dev/}p2 392 | dmsetup remove /dev/mapper/${freeloop#/dev/}p1 393 | losetup -d $freeloop && touch "${TARGETDIR}/.bootstrap.success" 394 | fi 395 | 396 | # First run 397 | 398 | # apt install qemu-system-x86 qemu 399 | qemu-system-x86_64 -enable-kvm -smp cpus="$CPUS" -m "$MEM" -drive \ 400 | file="${TARGETDIR}"/disk.img,if=virtio,format=raw \ 401 | -pidfile "${TARGETDIR}/qemu.pid" \ 402 | $NET $DAEMONIZE $EXTRAS \ 403 | -vnc "$VNC" 404 | retval="$?" 405 | if [ "$retval" -lt 1 ] ; then 406 | echo "Successfully started, use" 407 | echo "" 408 | echo " vncviewer localhost${VNC}" 409 | echo "" 410 | echo "to get a console." 411 | else 412 | echo "" 413 | echo "Ooopsi." 414 | echo "Start failed, please check your configuration." 415 | fi 416 | -------------------------------------------------------------------------------- /docserve/README.md: -------------------------------------------------------------------------------- 1 | # Application server for asciidoc 2 | 3 | This might grow to a live doc server for the Checkmk documentation written in Asciidoc. It's main purpose is to make working on the docs easier. A second use case might be making the build/export process easier. 4 | 5 | Use at your own risk. 6 | 7 | ## Requires: 8 | 9 | * webrick gem 10 | * nokogiri gem 11 | * tilt gem 12 | * slim gem 13 | * concurrent-ruby gem (recommended) 14 | * asciidoctor executable 15 | * asciidoctor-diagram gem 16 | * pygments.rb gem 17 | * hunspell gem 18 | 19 | Due to a bug your checkmkdocs-styling repo need two empty menu files: 20 | 21 | ``` 22 | cd checkmkdocs-styling 23 | for l in de en ; do 24 | mkdir ${l} 25 | touch ${l}/menu.html.slim 26 | done 27 | ``` 28 | 29 | Usage: 30 | 31 | ``` 32 | cd docserve 33 | sudo gem install webrick # pretending everything for asciidoctor is already there 34 | ruby docserve.rb --docs ~/git/checkmk-docs --styling ~/git/checkmkdocs-styling --cache /tmp/doccache 35 | firefox http://localhost:8088/ 36 | ``` 37 | 38 | You might use a JSON config file, either searched as `$HOME/.config/checkmk-docserve.cfg` or in the program directory or specified via the CLI option `--config` followed by the path to the configuration file. 39 | 40 | ## Run in background 41 | 42 | As of now no systemd unit file is provided, you might just start it via your `/etc/rc.local` - here as user "harry": 43 | 44 | ``` 45 | screen -dmS docserve su harry -c "ruby /full/path/to/docserve.rb --config /full/path/to/myconfig.cfg" 46 | ``` 47 | 48 | Or in the startup file of your session manager that is already executed with user privileges: 49 | 50 | ``` 51 | screen -dmS docserve ruby /full/path/to/docserve.rb --config /full/path/to/myconfig.cfg 52 | ``` 53 | 54 | This starts a screen session with the docserve script running inside. You can connect to this screen session to view console output. 55 | -------------------------------------------------------------------------------- /docserve/autoreload.js: -------------------------------------------------------------------------------- 1 | // JS snippet to autoreload: 2 | 3 | var lastChange = CHANGED; 4 | var checkUrl = "JSONURL"; 5 | var autoReload = setInterval(checkForUpdate, 1000); 6 | 7 | function checkForUpdate() { 8 | var xhr = new XMLHttpRequest(); 9 | var j; 10 | xhr.open('GET', checkUrl, true); 11 | xhr.responseType = 'json'; 12 | xhr.onload = function() { 13 | var status = xhr.status; 14 | if (status === 200) { 15 | // console.log(xhr.response["last-change"]); 16 | // console.log(lastChange); 17 | if (xhr.response["last-change"] > lastChange) { 18 | console.log("Page has changed, reloading."); 19 | clearInterval(autoReload); 20 | location.reload(true); 21 | } 22 | } 23 | }; 24 | xhr.send(); 25 | } 26 | -------------------------------------------------------------------------------- /docserve/auxiliary/DocserveAuxiliary.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # encoding: utf-8 3 | # 4 | # (C) 2022, 2023 Mattias Schlenker for Checkmk GmbH 5 | 6 | require 'fileutils' 7 | require 'optparse' 8 | require 'nokogiri' 9 | require 'json' 10 | require 'net/http' 11 | require 'net/https' 12 | require 'uri' 13 | require 'json' 14 | 15 | class DocserveAuxiliary 16 | 17 | def DocserveAuxiliary.get_config 18 | cfg = {} 19 | # Configuration either from cmdline arguments or from cfg file 20 | cfg['basepath'] = nil # Path to the checkmk-docs directory 21 | cfg['templates'] = nil # Path to the checkmkdocs-styling directory 22 | cfg['cachedir'] = nil # Path to the cache directory, needed for the menu 23 | cfg['port'] = 8088 # Port to use 24 | cfg['cfgfile'] = nil 25 | cfg['injectcss'] = [] 26 | cfg['injectjs'] = [] 27 | cfg['checklinks'] = 1 28 | cfg['spelling'] = 1 29 | # Pre-build all files (for statistics and faster caching) 30 | cfg['buildall'] = 0 31 | # Run in batch mode: Build only the documents requested, print out errors and exit accordingly 32 | cfg['batchmode'] = 0 33 | # Auto detect files to build 34 | cfg['since'] = nil 35 | # For posting to slack 36 | cfg['slackauth'] = nil 37 | cfg['channel'] = nil 38 | # Some files to log to 39 | cfg['linklog'] = nil 40 | # Compare the structure of both languages 41 | cfg['structure'] = 0 42 | # Build the SaaS User Guide 43 | cfg['saas'] = 0 44 | cfg['newdir'] = nil 45 | # Create a list of files to build at boot 46 | cfg['prebuild'] = [] 47 | # Languages to build 48 | cfg['languages'] = [ 'en', 'de' ] 49 | # Output directory for batch build 50 | cfg['outdir'] = nil 51 | # Branches for batch builds 52 | cfg['branches'] = nil 53 | cfg['build_branches'] = nil 54 | # Default branch 55 | cfg['default'] = nil 56 | # Map virtual branches to nonexisting branches: 57 | cfg['fake_branches'] = {} 58 | # Use cached supported builds? 59 | cfg['cached_supported'] = 1 60 | # This should be moved to another object: 61 | cfg['toctitle'] = { 62 | "en" => "On this page", 63 | "de" => "Auf dieser Seite" 64 | } 65 | cfg['usedcss'] = [ 66 | "css/checkmk.css", "css/pygments-monokai.css", 67 | "css/addons.css", "css/SourceCodePro.css" 68 | ] 69 | cfg['usedjs'] = [ 70 | "js/manifest.js", "js/vendor.js", "js/app.js", 71 | "js/lunr.js", "js/lunr.stemmer.support.js", 72 | "js/lunr.de.js", "js/lunr.client.js" 73 | ] 74 | cfg['baseurl'] = 'https://docs.checkmk.com/' 75 | cfg['outdated'] = [ '1.6.0', '2.0.0' ] 76 | cfg['update_matrix'] = [ '1.5.0', '1.6.0', '2.0.0', '2.1.0', '2.2.0' ] 77 | 78 | opts = OptionParser.new 79 | opts.on('-s', '--styling', :REQUIRED) { |i| cfg['templates'] = i } 80 | opts.on('-d', '--docs', :REQUIRED) { |i| cfg['basepath'] = i } 81 | opts.on('-c', '--cache', :REQUIRED) { |i| cfg['cachedir'] = i } 82 | opts.on('-p', '--port', :REQUIRED) { |i| cfg['port'] = i } 83 | opts.on('--config', :REQUIRED) { |i| cfg['cfgfile'] = i } 84 | opts.on('--baseurl', :REQUIRED) { |i| cfg['baseurl'] = i } 85 | opts.on('--inject-css', :REQUIRED) { |i| cfg['injectcss'] = i.split(",") } 86 | opts.on('--inject-js', :REQUIRED) { |i| cfg['injectjs'] = i.split(",") } 87 | opts.on('--check-links', :REQUIRED) { |i| cfg['checklinks'] = i.to_i} 88 | opts.on('--spelling', :REQUIRED) { |i| cfg['spelling'] = i.to_i} 89 | opts.on('--build-all', :REQUIRED) { |i| cfg['buildall'] = i.to_i} 90 | opts.on('--batch', :REQUIRED) { |i| cfg['batchmode'] = i.to_i} 91 | opts.on('--pre-build', :REQUIRED) { |i| cfg['prebuild'] = i.split(",")} 92 | opts.on('--since', :REQUIRED) { |i| cfg['since'] = i.to_s} 93 | opts.on('--slack-auth', :REQUIRED) { |i| cfg['slackauth'] = i.to_s} 94 | opts.on('--channel', :REQUIRED) { |i| cfg['channel'] = i.to_s} 95 | opts.on('--linklog', :REQUIRED) { |i| cfg['linklog'] = i.to_s} 96 | opts.on('--structure', :REQUIRED) { |i| cfg['structure'] = i.to_i} 97 | opts.on('--saas', :REQUIRED) { |i| cfg['saas'] = i.to_i} 98 | opts.on('--cached-supported', :REQUIRED) { |i| cfg['cached_supported'] = i.to_i} 99 | opts.on('--languages', :REQUIRED) { |i| cfg['languages'] = i.split(",")} 100 | opts.on('--outdir', :REQUIRED) { |i| cfg['outdir'] = i.to_s} 101 | opts.on('--branches', :REQUIRED) { |i| cfg['branches'] = i.split(",")} 102 | opts.on('--build-branches', :REQUIRED) { |i| cfg['build_branches'] = i.split(",")} 103 | opts.on('--virtual-branches', :REQUIRED) { |i| 104 | pairs = i.split(",") 105 | pairs.each { |p| 106 | ptoks = p.split('=') 107 | cfg['fake_branches'][ptoks[0]] = ptoks[1] 108 | } 109 | } 110 | opts.on('--default', :REQUIRED) { |i| cfg['default'] = i.to_s} 111 | opts.on('--new-dir-structure', :REQUIRED) { |i| cfg['newdir'] = i.to_i} 112 | opts.parse! 113 | 114 | unless cfg['cfgfile'].nil? 115 | jcfg = JSON.parse(File.read(cfg['cfgfile'])) 116 | jcfg.each { |k, v| 117 | cfg[k] = v 118 | } 119 | end 120 | if cfg['newdir'].nil? 121 | cfg['newdir'] = DocserveAuxiliary.identify_dir_structure(cfg) 122 | end 123 | [ 'templates', 'basepath', 'cachedir' ].each { |o| 124 | if cfg[o].nil? 125 | puts "At least specify: --styling --docs --cache " 126 | exit 1 127 | else 128 | cfg[o] = File.expand_path(cfg[o]) 129 | end 130 | } 131 | cfg['outdir'] = cfg['cachedir'] + '/out' if cfg['outdir'].nil? 132 | cfg['srcpath'] = cfg['basepath'] 133 | cfg['srcpath'] = cfg['cachedir'] + '/src' if cfg['newdir'] > 0 134 | cfg['build_branches'] = cfg['branches'] if cfg['build_branches'].nil? 135 | return cfg 136 | end 137 | 138 | def DocserveAuxiliary.create_file_list(cfg, idx=false) 139 | all_allowed = [] 140 | html = [] 141 | images = [] 142 | index = {} 143 | buildfiles = {} 144 | disallow = {} 145 | cfg['languages'].each { |lang| 146 | buildfiles[lang] = [] 147 | index[lang] = [] 148 | disallow[lang] = [] 149 | } 150 | if cfg['newdir'] < 1 151 | # Allow all asciidoc files except includes and menus 152 | cfg['languages'].each { |lang| 153 | Dir.entries(cfg['basepath'] + "/" + lang).each { |f| 154 | if f =~ /\.asciidoc/ 155 | fname = "/latest/" + lang + "/" + f.sub(/\.asciidoc$/, ".html") 156 | jname = "/last_change/latest/" + lang + "/" + f.sub(/\.asciidoc$/, ".html") 157 | unless f =~ /^(include|menu)/ 158 | all_allowed.push fname 159 | all_allowed.push jname 160 | html.push fname 161 | buildfiles[lang].push f 162 | if f =~ /^draft/ 163 | disallow[lang].push f 164 | elsif DocserveAuxiliary.decide_index(cfg['basepath'] + "/" + lang + "/" + f, idx) == true 165 | index[lang].push f 166 | else 167 | disallow[lang].push f 168 | end 169 | else 170 | disallow[lang].push f 171 | end 172 | end 173 | } 174 | } 175 | else 176 | subdirs = [ "common", "onprem" ] 177 | subdirs = [ "common", "saas" ] if $saas > 0 178 | cfg['languages'].each { |lang| 179 | subdirs.each { |d| 180 | Dir.entries(cfg['basepath'] + "/src/" + d + "/" + lang).each { |f| 181 | if f =~ /\.asciidoc/ 182 | fname = "/latest/" + lang + "/" + f.sub(/\.asciidoc$/, ".html") 183 | jname = "/last_change/latest/" + lang + "/" + f.sub(/\.asciidoc$/, ".html") 184 | unless f =~ /^(include|menu)/ 185 | all_allowed.push fname 186 | all_allowed.push jname 187 | html.push fname 188 | buildfiles[lang].push f 189 | if f =~ /^draft/ 190 | disallow[lang].push f 191 | elsif DocserveAuxiliary.decide_index(cfg['basepath'] + "/" + lang + "/" + f, idx) == true 192 | index[lang].push f 193 | else 194 | disallow[lang].push f 195 | end 196 | end 197 | end 198 | } 199 | } 200 | } 201 | end 202 | # Allow all images, but change their paths to include the language 203 | Dir.entries(cfg['basepath'] + "/images").each { |f| 204 | if f =~ /\.(png|jpeg|jpg|svg)$/ 205 | all_allowed.push "/latest/images/" + f 206 | images.push "../images/" + f 207 | end 208 | } 209 | # Allow all icons 210 | Dir.entries(cfg['basepath'] + "/images/icons").each { |f| 211 | if f =~ /\.(png|jpeg|jpg|svg)$/ 212 | all_allowed.push "/latest/images/icons/" + f 213 | images.push "../images/icons/" + f 214 | end 215 | } 216 | # Allow all files in any subdirectory in assets 217 | Dir.entries(cfg['templates'] + "/assets").each { |d| 218 | if File.directory?(cfg['templates'] + "/assets/" + d) 219 | unless d =~ /^\./ 220 | Dir.entries(cfg['templates'] + "/assets/" + d).each { |f| 221 | all_allowed.push "/assets/" + d + "/" + f if File.file?(cfg['templates'] + "/assets/" + d + "/" + f) 222 | } 223 | end 224 | end 225 | } 226 | # Allow the lunr index 227 | cfg['languages'].each { |lang| 228 | all_allowed.push "/latest/lunr.index.#{lang}.js" 229 | } 230 | all_allowed.push "/favicon.ico" 231 | all_allowed.push "/favicon.png" 232 | all_allowed.push "/errors.csv" 233 | all_allowed.push "/errors.html" 234 | all_allowed.push "/wordcount.html" 235 | all_allowed.push "/images.html" 236 | all_allowed.push "/images.txt" 237 | all_allowed.push "/links.html" 238 | all_allowed.push "/latest/index.html" 239 | all_allowed.push "/latest/" 240 | all_allowed.push "/latest" 241 | return { 242 | 'all_allowed' => all_allowed, 243 | 'html' => html, 244 | 'images' => images, 245 | 'buildfiles' => buildfiles, 246 | 'index' => index, 247 | 'disallow' => disallow 248 | } 249 | end 250 | 251 | def DocserveAuxiliary.decide_index(fpath, idx) 252 | return false if idx == false 253 | File.open(fpath).each { |line| 254 | return false if line =~ /\/\/\s*REDIRECT-PERMANENT/ 255 | return false if line =~ /\/\/\s*NO-LUNR/ 256 | } 257 | return true 258 | end 259 | 260 | def DocserveAuxiliary.create_softlinks(cfg) 261 | return if cfg['newdir'] < 1 262 | subdirs = [ "includes", "common", "onprem" ] 263 | subdirs = [ "includes", "common", "saas" ] if $saas > 0 264 | cfg['languages'].each { |lang| 265 | FileUtils.mkdir_p "#{cfg['cachedir']}/src/#{lang}" 266 | subdirs.each { |d| 267 | FileUtils.ln_s(Dir.glob("#{cfg['basepath']}/src/#{d}/#{lang}/*.a*doc"), 268 | "#{cfg['cachedir']}/src/#{lang}", force: true) 269 | FileUtils.ln_s(Dir.glob("#{cfg['basepath']}/src/#{d}/#{lang}/*.xml"), 270 | "#{cfg['cachedir']}/src/#{lang}", force: true) 271 | FileUtils.ln_s(Dir.glob("#{cfg['basepath']}/src/#{d}/#{lang}/*.txt"), 272 | "#{cfg['cachedir']}/src/#{lang}", force: true) 273 | } 274 | FileUtils.ln_s(Dir.glob("#{cfg['basepath']}/src/code/*.a*doc"), 275 | "#{cfg['cachedir']}/src/#{lang}", force: true) 276 | } 277 | end 278 | 279 | def DocserveAuxiliary.identify_dir_structure(cfg) 280 | if File.directory? "#{cfg['basepath']}/src/onprem/en" 281 | return 1 282 | elsif File.directory? "#{cfg['basepath']}/en" 283 | return 0 284 | end 285 | return nil 286 | end 287 | 288 | def DocserveAuxiliary.prepare_menu(cfg, branch='localdev') 289 | cfg['languages'].each { |lang| 290 | FileUtils.mkdir_p "#{cfg['cachedir']}/#{branch}/#{lang}" 291 | comm = "asciidoctor -T \"#{cfg['templates']}/templates/index\" -E slim \"#{cfg['srcpath']}/#{lang}/menu.asciidoc\" -D \"#{cfg['cachedir']}/#{branch}/#{lang}\"" 292 | system comm 293 | } 294 | end 295 | 296 | def DocserveAuxiliary.build_full(cfg, branch='localdev', lang='en', files=nil) 297 | files = DocserveAuxiliary.create_file_list(cfg) if files.nil? 298 | f = files['buildfiles'][lang].map { |f| "\"#{cfg['srcpath']}/#{lang}/#{f}\"" } 299 | b = cfg['build_branches'].join(' ') 300 | allfiles = f.join(' ') 301 | outdir = "#{cfg['outdir']}/#{branch}" 302 | outdir = "#{cfg['outdir']}/latest" if cfg['default'] == branch 303 | FileUtils.mkdir_p "#{outdir}" 304 | FileUtils.mkdir_p "#{outdir}/images/icons" 305 | FileUtils.mkdir_p "#{outdir}/#{lang}" 306 | comm = "asciidoctor -a toc-title=\"#{cfg['toctitle'][lang]}\" -a latest=\"#{cfg['default']}\" -a branches=\"#{b}\" -a branch=#{branch} -a lang=#{lang} -a jsdir=../../assets/js -a download_link=https://checkmk.com/#{lang}/download -a linkcss=true -a stylesheet=checkmk.css -a stylesdir=../../assets/css -T \"#{cfg['templates']}/templates/slim\" -E slim -a toc=right -D \"#{outdir}/#{lang}\" #{allfiles}" 307 | system comm 308 | end 309 | 310 | def DocserveAuxiliary.build_4_lunr(cfg, branch='localdev', lang='en', files=nil) 311 | files = DocserveAuxiliary.create_file_list(cfg) if files.nil? 312 | return if files['index'][lang].size < 1 313 | f = files['index'][lang].map { |f| "\"#{cfg['srcpath']}/#{lang}/#{f}\"" } 314 | allfiles = f.join(' ') 315 | FileUtils.mkdir_p "#{cfg['cachedir']}/lunr/#{branch}/#{lang}" 316 | comm = "asciidoctor -D \"#{cfg['cachedir']}/lunr/#{branch}/#{lang}\" #{allfiles}" 317 | system comm 318 | end 319 | 320 | def DocserveAuxiliary.build_lunr_index(cfg, branch='localdev', lang='en') 321 | outdir = "#{cfg['outdir']}/#{branch}" 322 | outdir = "#{cfg['outdir']}/latest" if cfg['default'] == branch 323 | FileUtils.mkdir_p "#{cfg['cachedir']}/lunr/#{branch}/#{lang}" 324 | comm = "node \"#{cfg['templates']}/lunr/build_index_#{lang}.js\" \"#{cfg['cachedir']}/lunr/#{branch}/#{lang}\" \"#{outdir}/lunr.index.#{lang}.js\"" 325 | system comm 326 | end 327 | 328 | def DocserveAuxiliary.switch_branch(cfg, branch='master', pull=false) 329 | b = branch 330 | b = cfg['fake_branches'][branch] if cfg['fake_branches'].has_key?(branch) 331 | pwd = Dir.pwd 332 | Dir.chdir cfg['basepath'] 333 | system('git pull') if pull == true 334 | system("git checkout \"#{b}\"") 335 | cfg['newdir'] = DocserveAuxiliary.identify_dir_structure(cfg) 336 | DocserveAuxiliary.create_softlinks(cfg) if cfg['newdir'] > 0 337 | Dir.chdir pwd 338 | return cfg 339 | end 340 | 341 | def DocserveAuxiliary.copy_images(cfg, branch='master') 342 | outdir = "#{cfg['outdir']}/#{branch}" 343 | outdir = "#{cfg['outdir']}/latest" if cfg['default'] == branch 344 | [ "/images", "/images/icons" ].each { |d| 345 | Dir.entries(cfg['basepath'] + d).each { |f| 346 | if File.file?(cfg['basepath'] + d + "/" + f) 347 | unless f =~ /(_orig|_original)\./ || f =~ /\.xcf$/ || f =~ /\.xcf\.gz$/ 348 | FileUtils.cp(cfg['basepath'] + d + "/" + f, outdir + d) 349 | end 350 | end 351 | } 352 | } 353 | end 354 | 355 | def DocserveAuxiliary.dedup_images(cfg, branches=nil) 356 | branches = cfg['build_branches'] if branches.nil? 357 | branches.push("latest") 358 | imgsums = {} 359 | pwd = Dir.pwd 360 | Dir.chdir cfg['outdir'] 361 | branches.each { |branch| 362 | IO.popen("sha256deep -rle \"#{branch}/images\"") { |l| 363 | while l.gets 364 | ltoks = $_.strip.split(" ") 365 | imgsums[ltoks[0]] = [] unless imgsums.has_key? ltoks[0] 366 | imgsums[ltoks[0]].push ltoks[1] 367 | end 368 | } 369 | } 370 | Dir.chdir pwd 371 | FileUtils.mkdir_p "#{cfg['outdir']}/common" 372 | imgsums.each { |k, v| 373 | if v.size > 1 374 | puts "Deduplicating: " + v.join(', ') 375 | FileUtils.cp(cfg['outdir'] + "/" + v[0], cfg['outdir'] + "/common/" + k) unless File.exist?(cfg['outdir'] + "/common/" + k) 376 | v.each { |i| 377 | upd = '../../common/' 378 | upd = '../../../common/' if i =~ /images\/icons\// 379 | FileUtils.rm(cfg['outdir'] + "/" + i) 380 | FileUtils.ln_sf(upd + k, cfg['outdir'] + "/" + i) 381 | } 382 | end 383 | } 384 | end 385 | 386 | def DocserveAuxiliary.copy_assets(cfg) 387 | dirs = [ "assets/css", "assets/js", ".well-known", 'assets/fonts', 'assets/images' ] 388 | cfg['languages'].each { |lang| dirs.push lang } 389 | dirs.each { |d| 390 | FileUtils.mkdir_p "#{cfg['outdir']}/#{d}" 391 | } 392 | comms = [] 393 | [ 'fonts/', 'images/' ].each { |d| 394 | comms.push "rsync -avHP --inplace \"#{cfg['templates']}/assets/#{d}/\" \"#{cfg['outdir']}/assets/#{d}/\"" 395 | } 396 | cfg['usedcss'].each { |f| 397 | comms.push "rsync -avHP --inplace \"#{cfg['templates']}/assets/#{f}\" \"#{cfg['outdir']}/assets/css/\"" 398 | } 399 | cfg['usedjs'].each { |f| 400 | comms.push "rsync -avHP --inplace \"#{cfg['templates']}/assets/#{f}\" \"#{cfg['outdir']}/assets/js/\"" 401 | } 402 | [ "assets/images/favicon.png", "main-sitemap.xsl" ].each { |f| 403 | comms.push "rsync -avHP --inplace \"#{cfg['templates']}/#{f}\" \"#{cfg['outdir']}/\"" 404 | } 405 | comms.push "rsync -avHP --inplace \"#{cfg['cachedir']}/supported_builds_clean.json\" \"#{cfg['outdir']}/assets/js/\"" 406 | comms.push "rsync -avHP --inplace \"#{cfg['templates']}/static/traffic-advice.json\" \"#{cfg['outdir']}/.well-known/traffic-advice\"" 407 | cfg['languages'].each { |lang| 408 | comms.push "rsync -avHP --inplace \"#{cfg['templates']}//opensearch/opensearch_#{lang}.xml\" \"#{cfg['outdir']}/#{lang}/opensearch.xml\"" 409 | } 410 | DocserveAuxiliary.retrieve_supported(cfg) 411 | comms.each { |c| system c } 412 | end 413 | 414 | def DocserveAuxiliary.nicify_startpage(cfg, branch='master', lang='en', hdoc=nil) 415 | return hdoc unless File.exist?(cfg['basepath'] + "/" + lang + "/featured_000.xml") 416 | return hdoc unless File.exist?(cfg['basepath'] + "/" + lang + "/landingpage.xml") 417 | writeback = false 418 | if hdoc.nil? 419 | writeback = true 420 | outdir = "#{cfg['outdir']}/#{branch}" 421 | outdir = "#{cfg['outdir']}/latest" if cfg['default'] == branch 422 | html = File.read("#{outdir}/#{lang}/index.html") 423 | hdoc = Nokogiri::HTML.parse html 424 | end 425 | begin 426 | # Extract the featured topic overlay 427 | featured = Nokogiri::HTML.parse(File.read(cfg['basepath'] + "/" + lang + "/featured_000.xml")) 428 | overlay = featured.css("div[id='topicopaque']") 429 | # Extract the new startpage layout 430 | landing = Nokogiri::HTML.parse(File.read(cfg['basepath'] + "/" + lang + "/landingpage.xml")) 431 | header = landing.css("div[id='header']") 432 | # Extract the column for featured topic 433 | ftcol = featured.css("div[id='featuredtopic']")[0] 434 | fttgt = landing.css("div[id='featuredtopic']")[0] 435 | fttgt.replace(ftcol) 436 | rescue 437 | # Nothing modified at this point 438 | return hdoc 439 | end 440 | hdoc.search(".//main[@class='home']//div[@id='header']").remove 441 | hdoc.search(".//main[@class='home']//div[@id='content']").remove 442 | hdoc.search(".//body[@class='article']//div[@id='header']").remove 443 | hdoc.search(".//body[@class='article']//div[@id='content']").remove 444 | main = hdoc.css("main[@class='home']")[0] 445 | main = hdoc.css("body[@class='article']")[0] if main.nil? 446 | # puts main 447 | main.add_child overlay 448 | main.add_child header 449 | # Identify the container in the target 450 | content = landing.css("div[id='content']") 451 | main.add_child content 452 | # Get autolists 453 | [ "most_visited", "recently_added", "recently_updated" ].each { |f| 454 | h, ul, hdoc = DocserveAuxiliary.get_autolist(cfg, f, hdoc, lang) 455 | lists = hdoc.css("div[id='autolists']")[0] 456 | lists.add_child h 457 | lists.add_child ul 458 | } 459 | if writeback == true 460 | outf = File.new("#{outdir}/#{lang}/index.html", 'w') 461 | outf.write hdoc.to_s 462 | outf.close 463 | end 464 | return hdoc 465 | end 466 | 467 | def DocserveAuxiliary.nicify_startpage_lunr(cfg, branch='master', lang='en') 468 | hdoc = Nokogiri::HTML.parse(File.read("#{cfg['cachedir']}/lunr/#{branch}/#{lang}/index.html")) 469 | hdoc = DocserveAuxiliary.nicify_startpage(cfg, branch, lang, hdoc) 470 | out = File.new("#{cfg['cachedir']}/lunr/#{branch}/#{lang}/index.html", "w") 471 | out.write hdoc 472 | out.close 473 | end 474 | 475 | def DocserveAuxiliary.get_autolist(cfg, name, hdoc, lang) 476 | h = nil 477 | ul = Nokogiri::XML::Node.new "ul", hdoc 478 | File.open(cfg['basepath'] + "/" + lang + "/" + name + ".txt").each { |line| 479 | if line =~ /^\#/ || line.strip == "" 480 | # do nothing 481 | elsif line =~ /^=\s/ 482 | h = Nokogiri::XML::Node.new "h4", hdoc 483 | h.content = line.strip.sub(/^=\s/, "") 484 | else 485 | fname = line.strip 486 | File.open(cfg['basepath'] + "/" + lang + "/" + fname + ".asciidoc").each { |aline| 487 | if aline =~ /^=\s/ 488 | li = Nokogiri::XML::Node.new "li", hdoc 489 | a = Nokogiri::XML::Node.new "a", hdoc 490 | a.content = aline.strip.sub(/^=\s/, "").gsub("{CMK}", "Checkmk") 491 | a["href"] = fname + ".html" 492 | li.add_child a 493 | ul.add_child li 494 | end 495 | } 496 | end 497 | } 498 | return h, ul, hdoc 499 | end 500 | 501 | def DocserveAuxiliary.generate_sitemap(cfg, branch, files) 502 | # Only build the sitemap for the default branch 503 | return unless cfg['default'] == branch 504 | skipfiles = [] 505 | if File.exist?(cfg['basepath'] + "/exclude_from_search.txt") 506 | File.open(cfg['basepath'] + "/exclude_from_search.txt").each { |line| 507 | cfg['languages'].each { |l| 508 | skipfiles.push(l + "/" + line.strip + ".asciidoc") unless line.strip =~ /^\#/ 509 | } 510 | } 511 | end 512 | moddates = {} 513 | pwd = Dir.pwd 514 | Dir.chdir cfg['basepath'] 515 | cfg['languages'].each { |lang| 516 | moddates[lang] = {} 517 | files['index'][lang].each { |f| 518 | unless skipfiles.include? f 519 | lastmod = ` /usr/bin/git log -1 --pretty="format:%ci" "#{lang}/#{f}" ` 520 | if lastmod == "" 521 | lastmod = Time.new.strftime("%Y-%m-%d") 522 | end 523 | puts lastmod 524 | moddates[lang][f] = lastmod 525 | end 526 | } 527 | } 528 | Dir.chdir pwd 529 | DocserveAuxiliary.dump_sitemap(cfg, moddates) 530 | DocserveAuxiliary.dump_robots(cfg, files, skipfiles) 531 | end 532 | 533 | def DocserveAuxiliary.dump_sitemap(cfg, entries) 534 | outfile = File.new("#{cfg['outdir']}/sitemap.xml", 'w') 535 | outfile.write "" + 536 | "\n" + 537 | "\n" 543 | cfg['languages'].each { |lang| 544 | entries[lang].each { |f,t| 545 | link = cfg['baseurl'] + "latest/" + lang + "/" + f.gsub(".asciidoc", ".html") 546 | outfile.write "\n" + link + "\n" 547 | cfg['languages'].each { |l| 548 | if entries[l].has_key? f 549 | link = cfg['baseurl'] + "latest/" + l + "/" + f.gsub(".asciidoc", ".html") 550 | outfile.write "\n" 551 | end 552 | } 553 | outfile.write "" + t[0..9] + "\n" 554 | outfile.write "monthly\n0.7\n\n" 555 | } 556 | } 557 | outfile.write "\n" 558 | outfile.close 559 | end 560 | 561 | def DocserveAuxiliary.dump_robots(cfg, files, skipfiles) 562 | outfile = File.new("#{cfg['outdir']}/robots.txt", 'w') 563 | outfile.write "User-agent: *\nAllow: /\nDisallow: /master/\n" 564 | lines = [] 565 | cfg['outdated'].each { |o| 566 | lines.push "Disallow: /#{o}/\n" 567 | } 568 | lines.push "# Files listed in exclude_from_search.txt are disallowed:\n" 569 | skipfiles.each { |f| 570 | f = f.gsub(".asciidoc", ".html") 571 | lines.push "Disallow: /latest/#{f}\n" 572 | } 573 | lines.push "# Files with stub content triggering disallow:\n" 574 | cfg['languages'].each { |l| 575 | files['disallow'][l].each { |f| 576 | f = f.gsub(".asciidoc", ".html") 577 | lines.push "Disallow: /latest/#{l}/#{f}\n" 578 | } 579 | } 580 | lines.uniq.each { |l| outfile.write l } 581 | outfile.write "# Sitemap:\n" 582 | outfile.write "Sitemap: #{cfg['baseurl']}/sitemap.xml\n" 583 | outfile.close 584 | end 585 | 586 | def DocserveAuxiliary.retrieve_supported(cfg) 587 | if cfg['cached_supported'] > 0 && File.exist?("#{cfg['cachedir']}/supported_builds_clean.json") 588 | return 589 | end 590 | url = URI("https://download.checkmk.com/supported_builds.json") 591 | resp = Net::HTTP.get_response(url) 592 | h = JSON.parse resp.body 593 | cleaned = {} 594 | cleaned["cre"] = {} 595 | cfg['update_matrix'].each { |r| 596 | cleaned["cre"][r] = h["cre"][r] 597 | } 598 | outfile = File.new("#{cfg['cachedir']}/supported_builds_clean.json", 'w') 599 | outfile.write(cleaned.to_json) 600 | outfile.close 601 | end 602 | 603 | end 604 | -------------------------------------------------------------------------------- /docserve/branding.dic: -------------------------------------------------------------------------------- 1 | 82 2 | 3ware 3 | agentenlosen 4 | Agentenplugins 5 | Automationsbenutzer 6 | Automationsbenutzers 7 | CEE 8 | checkmk 9 | Checkmk 10 | cmc 11 | CPE 12 | CRE 13 | CRIT 14 | cronjobs 15 | Datadog 16 | deregistrieren 17 | Deregistrierung 18 | ESXi 19 | Grafana 20 | https 21 | HW 22 | InfluxDB 23 | init 24 | Instanzbenutzer 25 | Instanzname 26 | integration 27 | integrations 28 | IPMI 29 | Jira 30 | JSON 31 | Kubernetes 32 | livestatus 33 | localdev 34 | Mattermost 35 | mitinstalliert 36 | mkp 37 | mkps 38 | MK's 39 | monitoring 40 | monitorings 41 | MRPE 42 | MRPEs 43 | Multipath 44 | Nagstamon 45 | NagVis 46 | NRPE 47 | ntopng 48 | omd 49 | OpenManage 50 | openSUSE 51 | opsgenie 52 | pagerduty 53 | paketieren 54 | pingt 55 | Registrierungsversuch 56 | RHEL 57 | SAML 58 | ServiceNow 59 | Shellbefehle 60 | Shellskript 61 | Skript 62 | Skripte 63 | slas 64 | SLES 65 | SNMP 66 | Softlink 67 | Splunk 68 | SSD 69 | STDERR 70 | STDOUT 71 | SuperDoctor 72 | Supermicros 73 | systemweit 74 | SysVinit 75 | TLS 76 | updaten 77 | UUID 78 | VMWare 79 | Webex 80 | WinSCP 81 | x86 82 | x86_64 83 | -------------------------------------------------------------------------------- /docserve/builddocs.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # encoding: utf-8 3 | # 4 | # (C) 2022, 2023 Mattias Schlenker for Checkmk GmbH 5 | 6 | require 'auxiliary/DocserveAuxiliary' 7 | 8 | s = Time.now.to_i 9 | 10 | cfg = DocserveAuxiliary.get_config 11 | DocserveAuxiliary.copy_assets(cfg) 12 | 13 | cfg['build_branches'].each { |b| 14 | cfg = DocserveAuxiliary.switch_branch(cfg, b) 15 | files = DocserveAuxiliary.create_file_list(cfg, true) 16 | DocserveAuxiliary.prepare_menu(cfg) 17 | ts = [] 18 | cfg['languages'].each { |lang| 19 | ts.push Thread.new{ DocserveAuxiliary.build_full(cfg, b, lang, files) } 20 | ts.push Thread.new{ DocserveAuxiliary.build_4_lunr(cfg, b, lang, files) } 21 | } 22 | ts.push Thread.new{ DocserveAuxiliary.generate_sitemap(cfg, b, files) } 23 | ts.push Thread.new{ DocserveAuxiliary.copy_images(cfg, b) } 24 | ts.each { |t| t.join } 25 | cfg['languages'].each { |lang| 26 | ts.push Thread.new{ DocserveAuxiliary.nicify_startpage_lunr(cfg, b, lang) } 27 | ts.push Thread.new{ DocserveAuxiliary.nicify_startpage(cfg, b, lang) } 28 | } 29 | ts.each { |t| t.join } 30 | ts = [] 31 | cfg['languages'].each { |lang| 32 | ts.push Thread.new{ DocserveAuxiliary.build_lunr_index(cfg, b, lang) } 33 | } 34 | ts.each { |t| t.join } 35 | } 36 | DocserveAuxiliary.dedup_images(cfg) 37 | 38 | e = Time.now.to_i 39 | d = e - s 40 | puts "Build took #{d} seconds." 41 | -------------------------------------------------------------------------------- /docserve/checkmk-docserve.cfg.example: -------------------------------------------------------------------------------- 1 | { 2 | "styling" : "/home/mschlenker/git/checkmkdocs-styling", 3 | "docs" : "/home/mschlenker/git/checkmk-docs", 4 | "cache" : "/tmp/docserve.cache", 5 | "port" : 8088, 6 | "inject-css" : [ "/path/to/file/with/css/to/inject" ], 7 | "inject-js" : [ "/path/to/file/with/js/to/inject" ], 8 | "check-links" : 1 9 | } 10 | -------------------------------------------------------------------------------- /docserve/docserve.css: -------------------------------------------------------------------------------- 1 | #docserveerrors { 2 | border-style: solid; 3 | border-width: 2px; 4 | border-color: red; 5 | padding: 5px; 6 | } 7 | 8 | .errmono { 9 | font-family: monospace; 10 | } 11 | 12 | span.hll span.tok-go, 13 | span.hll span.tok-n, 14 | span.hll span.tok-p, 15 | span.hll span.tok-s2, 16 | span.hll span.tok-nb { 17 | color: black; 18 | } 19 | 20 | div.header-top__search__results__breadcrumb { 21 | font-size: 80%; 22 | font-style: italic; 23 | } 24 | 25 | /* main { 26 | background-image: url(/assets/images/tile_draft.png); 27 | } */ 28 | 29 | /* min-width is only used for centering on landing page */ 30 | @media only screen and (min-width: 1650px) and (min-height: 980px) { 31 | 32 | } 33 | /* responsive stuff for smaller displays */ 34 | 35 | @media only screen and (max-width: 1420px) { 36 | 37 | } 38 | 39 | @media only screen and (max-width: 1200px) { 40 | 41 | } 42 | 43 | @media only screen and (max-width: 992px) { 44 | 45 | } 46 | 47 | @media only screen and (max-width: 768px) { 48 | 49 | } 50 | 51 | @media only screen and (max-width: 576px) { 52 | 53 | } 54 | -------------------------------------------------------------------------------- /docserve/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschlenker/checkmk-snippets/49abc3923b248651f4cb67b8d346c13f2691d622/docserve/favicon.ico -------------------------------------------------------------------------------- /docserve/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschlenker/checkmk-snippets/49abc3923b248651f4cb67b8d346c13f2691d622/docserve/favicon.png -------------------------------------------------------------------------------- /docserve/fold.js: -------------------------------------------------------------------------------- 1 | /* Test for folding */ 2 | 3 | var menu = document.getElementsByClassName("main-nav__content")[0]; 4 | var subheaders = menu.getElementsByClassName("sect2"); 5 | 6 | for (var i=0; i { foldAllSiblings(); }); 46 | -------------------------------------------------------------------------------- /mkp/hellobakery/README.md: -------------------------------------------------------------------------------- 1 | # Hello bakery! 2 | 3 | > [!WARNING] 4 | > This example uses an API that was replaced in Checkmk 2.3.0 and will be completely disabled in Checkmk 2.4.0. 5 | > Use this example only to compare with the new examples as a porting aid. 6 | > The new examples have been moved to [the Checkmk docs repo](https://github.com/Checkmk/checkmk-docs/tree/master/examples/bakery_api). 7 | > Furthermore "Hello world" and "Hello bakery" have been merged again. 8 | 9 | This example extends the "Hello world!" example with bakery configuration 10 | to showcase how plugins can access the bakery API for easier distribution 11 | of agent checks and their configuration. 12 | 13 | Only for Checkmk Enterprise Editions! 14 | 15 | ## Contents 16 | 17 | A Python script for the agent side that outputs "hello_bakery" and a 18 | random number between 0.0 and 100.0. The Checkmk side changes the service 19 | status to WARN and CRIT when thresholds of 80.0 or 90.0 are reached (so 20 | expect such a service status roughly every 5 minutes). 21 | 22 | Additionally the user name read from the JSON configuration is printed out. 23 | 24 | ``` 25 | <<>> 26 | hello_bakery 90.24242012379389 27 | user johndoe 28 | ``` 29 | 30 | The package includes: 31 | 32 | - Checkmk side agent-based check 33 | - Linux agent plugin 34 | - Windows agent plugin 35 | - Configuration for editable thresholds 36 | - Definition of a graph 37 | - Definition of perf-o-meter 38 | - Registry for the bakery API 39 | - Bakery example with postinstall and configuration 40 | - Configuration of package contents via Checkmk Setup GUI 41 | -------------------------------------------------------------------------------- /mkp/hellobakery/mkp/_agent_based/hello_bakery.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Copyright (C) 2021 Mattias Schlenker for tribe29 GmbH 4 | # License: GNU General Public License v2 5 | # 6 | # Reference for details: 7 | # https://docs.checkmk.com/latest/en/devel_check_plugins.html 8 | # 9 | # This is the Checkmk server side script. 10 | 11 | from .agent_based_api.v1 import * 12 | 13 | # Most simple discovery function, basically just checks for the agent 14 | # output containing a section named exactly as the service: 15 | 16 | def discover_hello_bakery(section): 17 | yield Service() 18 | 19 | # Thresholds are taken from the default parameters defined in 20 | # ~/local/share/check_mk/web/plugins/wato/hellobakery_parameters.py 21 | # respectively overwritten by Setup GUI settings. 22 | 23 | def check_hello_bakery(params, section): 24 | for line in section: 25 | # Proving a metric is the first step to get nice graphs, adding these lines 26 | # already allows you to create custom graphs using "hellobakerylevel" 27 | # 28 | # If you want to define a default graph, look here: 29 | # ~/local/share/check_mk/web/plugins/metrics/hellobakery_metric.py 30 | # 31 | # Everyone loves perf-o-meters, so take the next step and build one! 32 | # ~/local/share/check_mk/web/plugins/perfometer/hellobakery_perfometer.py 33 | if line[0] == "hello_bakery": 34 | yield Metric(name="hellobakerylevel", value=float(line[1]), boundaries=(0.0, 100.0)) 35 | 36 | # Two very simple transitions, both are not hardcoded. 37 | # Here we have to cast the string received to float. To cast, use a function 38 | # that fails early. Checkmk catches exceptions and changes state to UNKNOWN. 39 | # This is much better than casting the level of your bathtub to 0% allowing 40 | # more water to be poured in and not recognizing there might be a problem 41 | # that has to be investigated. 42 | if float(line[1]) > params["levels"][1]: 43 | yield Result(state=State.CRIT, summary="Hello, leave me alone!") 44 | return 45 | elif float(line[1]) > params["levels"][0]: 46 | yield Result(state=State.WARN, summary="Hello, I need some coffee!") 47 | return 48 | yield Result(state=State.OK, summary="Hello bakery! What a lovely day.") 49 | 50 | # Register the plugin with a unique name and define at least three functions! 51 | # Default parameters can be left empty since defined elsewhere. 52 | 53 | register.check_plugin( 54 | # This is some unique identifier! Do not use too generic terms like "hello_world" or 55 | # "updates_pending" since someone else might have had your idea and your and his 56 | # or hers output is not parseable by your discovery/check function and vice versa! 57 | name = "hello_bakery", 58 | # Nice human readable name. Be descriptive! 59 | service_name = "Hello bakery!", 60 | # Refer to the discovery function above. 61 | discovery_function = discover_hello_bakery, 62 | # Refer to the check function above. 63 | check_function = check_hello_bakery, 64 | # Refer to the ruleset defined in: 65 | # ~/local/share/check_mk/web/plugins/wato/hellobakery_parameters.py 66 | check_ruleset_name = "hello_bakery", 67 | # check_default_parameters = {} 68 | # if defaults have to be defined here, use: 69 | check_default_parameters = { "levels" : (80.0, 90.0) } 70 | ) 71 | 72 | -------------------------------------------------------------------------------- /mkp/hellobakery/mkp/_agents/plugins/hello_bakery: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Copyright (C) 2021 Mattias Schlenker for tribe29 GmbH 4 | # License: GNU General Public License v2 5 | # 6 | # Reference for details: 7 | # https://docs.checkmk.com/latest/en/devel_check_plugins.html#includecommand 8 | # 9 | # This is the Checkmk agent side script 10 | # /usr/lib/check_mk_agent/plugins/hello_bakery 11 | # 12 | # For our "Hello bakery!" example we extended the "Hello world!" script to read 13 | # a JSON configuration file and also output the configured user name that was 14 | # set by agent plugin rules. 15 | 16 | from random import random 17 | import json 18 | 19 | # Read our config filed: 20 | with open('/etc/check_mk/hello_bakery.json') as json_file: 21 | cfg = json.load(json_file) 22 | 23 | # The section should just use 7bit lowercase characters and underscores. They must 24 | # match the name set in register.check_plugin(). Make sure to choose a unique name 25 | # to avoid having two plugins with the same name. 26 | 27 | print("<<>>") 28 | 29 | # Just print a (fairly) random number between 0.0 and 100.0, interpreted as 30 | # percentage. The output will be space separated which will be split into tokens 31 | # as CheckMK receives the output. 32 | # 33 | # hello_bakery 98.41417513129443 34 | 35 | print("hello_bakery", random() * 100.0) 36 | print("user", cfg['user']) 37 | -------------------------------------------------------------------------------- /mkp/hellobakery/mkp/_agents/windows/plugins/hello_bakery.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | set /a rand=(%random%*100/32768) 3 | echo ^<^<^<^hello_bakery^>^>^> 4 | echo hello_bakery %rand%.5 5 | -------------------------------------------------------------------------------- /mkp/hellobakery/mkp/_checkman/hello_bakery: -------------------------------------------------------------------------------- 1 | title: Hello bakery sample 2 | agents: hello_bakery 3 | author: Mattias Schlenker 4 | license: GPLv2 5 | agents: linux, windows 6 | distribution: 7 | description: 8 | "Hello bakery!" is an example package primarily built for the purpose 9 | of showcasing minimal contents of a valid, usable package that can 10 | be uploaded to the Checkmk exchange. In comparison to "Hello world!" 11 | it extends the structure to bakery API plugin files that allow for 12 | distribution of this package to hosts. The full example also allows 13 | for writing configuration files that include parameters set by agent 14 | rules and using post install or pre remove hooks. 15 | 16 | Expected agent output: 17 | 18 | <<>> 19 | hello_bakery 57.746 20 | 21 | The float is expected to be in the range between 0% and 100%. 22 | The Linux agent relies on the following four lines Python. 23 | Without bakery support, store it as 24 | /usr/lib/check_mk_agent/plugins/hello_bakery and make sure it is set 25 | executable. 26 | 27 | Minimal sample agent script: 28 | 29 | #!/usr/bin/env python3 30 | from random import random 31 | print("<<>>") 32 | print("hello_bakery", random() * 100.0) 33 | 34 | Please see the example in /usr/lib/check_mk_agent/plugins/hello_bakery for 35 | our extended script that also reads the config and outputs one of the 36 | parameters configured via Setup GUI. This "full" script will be passed to 37 | Linux clients in the baked DEB or RPM packages. 38 | 39 | In default settings, above 80% "hellobakerylevel" the state changes to 40 | WARN, issueing the message "Hello, I need some coffee!", 41 | above 90%, the state will be CRIT "Hello, leave me alone!". 42 | 43 | Feel free to use the files contained in this MKP as a foundation for 44 | your first proper Checkmk plugin. Have fun! 45 | -------------------------------------------------------------------------------- /mkp/hellobakery/mkp/_lib/check_mk/base/cee/plugins/bakery/hello_bakery.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Copyright (C) 2021 Mattias Schlenker for tribe29 GmbH 4 | # License: GNU General Public License v2 5 | # 6 | # Reference for details: 7 | # https://docs.checkmk.com/latest/en/bakery_api.html 8 | # 9 | # This file defines which files (binaries and configuration) will be added 10 | # to a Checkmk agent that is assembled with the agent bakery. 11 | 12 | import json 13 | from pathlib import Path 14 | from typing import TypedDict, List 15 | 16 | # Import a whole lot of the bakery API. This might be too much, but probably will 17 | # help you when extending our example. 18 | 19 | from .bakery_api.v1 import ( 20 | OS, 21 | DebStep, 22 | RpmStep, 23 | SolStep, 24 | Plugin, 25 | PluginConfig, 26 | # SystemBinary, # add when using the commented code parts 27 | Scriptlet, 28 | WindowsConfigEntry, 29 | register, 30 | FileGenerator, 31 | ScriptletGenerator, 32 | WindowsConfigGenerator, 33 | quote_shell_string, 34 | ) 35 | 36 | # Create a class that holds our config. This corresponds to the parameters set 37 | # in the setup GUI and defines in web/plugins/hellobakery_bakery.py. 38 | 39 | class HelloBakeryConfig(TypedDict, total=False): 40 | interval: int 41 | user: str 42 | content: str 43 | 44 | def get_hello_bakery_plugin_files(conf: HelloBakeryConfig) -> FileGenerator: 45 | # In some cases you may want to override user input here to ensure a minimal 46 | # interval! 47 | interval = conf.get('interval') 48 | 49 | # The source file, specified with "source" argument, is taken from 50 | # ~/local/share/check_mk/agents/plugins/. It will be installed under the target name, 51 | # specified with "target" argument, in /usr/lib/check_mk_agent/plugins// 52 | # or in /usr/lib/check_mk_agent/plugins/ (if synchronous call is requested) 53 | # on the target system. If the "target" argument is omitted, the "source" argument 54 | # will be reused as target name 55 | yield Plugin( 56 | base_os=OS.LINUX, 57 | source=Path('hello_bakery'), 58 | interval=interval, 59 | ) 60 | 61 | # This example skips an agent plugin for Solaris systems. If unsure whether 62 | # Python is present, you might want to add a Korn shell script instead: 63 | # 64 | #yield Plugin( 65 | # base_os=OS.SOLARIS, 66 | # source=Path('hello_bakery.solaris.ksh'), 67 | # target=Path('hello_bakery'), 68 | # interval=interval, 69 | #) 70 | 71 | # Install a CMD file for Windows (BAT/PS/VMS are recommended defaults): 72 | yield Plugin( 73 | base_os=OS.WINDOWS, 74 | source=Path('hello_bakery.cmd'), 75 | target=Path('hello_bakery.bat'), 76 | interval=interval, 77 | ) 78 | 79 | # Put a configuration file to the list for Linux systems: Here we assume that our plugin 80 | # uses a JSON file for configuration. Switch off the banner, since the banner would use 81 | # shell style comments (#), but JSON requires JavaScript like comments (/* ... */). 82 | yield PluginConfig(base_os=OS.LINUX, 83 | lines=_get_linux_cfg_lines(conf['user'], conf['content']), 84 | target=Path('hello_bakery.json'), 85 | include_header=False) 86 | 87 | # Put a configuration file to the list for Solaris systems: 88 | # If we build a configuration file that can be sourced as shell snippet, we can 89 | # keep the banner: 90 | # 91 | #yield PluginConfig(base_os=OS.SOLARIS, 92 | # lines=_get_solaris_cfg_lines(conf['user'], conf['content']), 93 | # target=Path('hello_bakery.cfg'), 94 | # include_header=True) 95 | 96 | # In some cases the agent needs to be accompanied by a binary. This dumps 97 | # the binary mentioned to the default binary directionary (typically /usr/bin) 98 | # and registers the file with the package manager. 99 | # 100 | #for base_os in [OS.LINUX]: 101 | # yield SystemBinary( 102 | # base_os=base_os, 103 | # source=Path('some_binary'), 104 | # ) 105 | 106 | def _get_linux_cfg_lines(user: str, content: str) -> List[str]: 107 | # Let's assume that our Linux example plugin uses JSON as configuration file format 108 | config = json.dumps({'user': user, 'content': content}) 109 | return config.split('\n') 110 | 111 | def _get_solaris_cfg_lines(user: str, content: str) -> List[str]: 112 | # To be loaded with 'source' in Solaris shell script 113 | return [ 114 | f'USER={quote_shell_string(user)}', 115 | f'CONTENT={quote_shell_string(user)}', 116 | ] 117 | 118 | # Depending on your preference you might wanna use pickle to dump the config 119 | # or write plain CSV... It's all up to you. Just make sure that configuration files 120 | # are always treated as an array of lines. 121 | 122 | # And now for the scriptlets. In Debian based distributions and Solaris, postinst/ 123 | # prerm etc. are files on their own. In RPM based systems all scriptlets are sections 124 | # in a larger file. Since each agent plugin has it's own few lines and the plugin in 125 | # general also has some to restart the job, everything will be concatenated. 126 | 127 | # Here be dragons: DO NOT issue "exit 0" as last line in your postinst/prerm etc. 128 | # files since the lines of all plugins that are included are concatenated and trailed 129 | # by the package management scripts that belong to the agent itself. Also never modify 130 | # the environment in a way that might break following scripts. If you have to change 131 | # directory, save the current directory as olddir=`pwd` and change back as last line. 132 | 133 | def get_hello_bakery_scriptlets(conf: HelloBakeryConfig) -> ScriptletGenerator: 134 | installed_lines = ['logger -p Checkmk_Agent "Installed hello_bakery"'] 135 | uninstalled_lines = ['logger -p Checkmk_Agent "Uninstalled hello_bakery"'] 136 | 137 | yield Scriptlet(step=DebStep.POSTINST, lines=installed_lines) 138 | yield Scriptlet(step=DebStep.POSTRM, lines=uninstalled_lines) 139 | yield Scriptlet(step=RpmStep.POST, lines=installed_lines) 140 | yield Scriptlet(step=RpmStep.POSTUN, lines=uninstalled_lines) 141 | yield Scriptlet(step=SolStep.POSTINSTALL, lines=installed_lines) 142 | yield Scriptlet(step=SolStep.POSTREMOVE, lines=uninstalled_lines) 143 | 144 | # Just because we can we will also write a Windows configuration. In contrast to 145 | # Unices, Windows configuration is kept in a centralized file, not in 146 | # individual files for each plugin. 147 | 148 | def get_hello_bakery_windows_config(conf: HelloBakeryConfig) -> WindowsConfigGenerator: 149 | yield WindowsConfigEntry(path=["hello_bakery", "user"], content=conf["user"]) 150 | yield WindowsConfigEntry(path=["hello_bakery", "content"], content=conf["content"]) 151 | 152 | register.bakery_plugin( 153 | name="hello_bakery", 154 | files_function=get_hello_bakery_plugin_files, 155 | scriptlets_function=get_hello_bakery_scriptlets, 156 | windows_config_function=get_hello_bakery_windows_config, 157 | ) 158 | -------------------------------------------------------------------------------- /mkp/hellobakery/mkp/_web/plugins/metrics/hellobakery_metric.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Copyright (C) 2021 Mattias Schlenker for tribe29 GmbH 4 | # License: GNU General Public License v2 5 | # 6 | # Reference for details: 7 | # https://docs.checkmk.com/latest/en/devel_check_plugins.html#ownmetricdefinitions 8 | # 9 | # Here we define the metrics for the graph. 10 | 11 | # Import everything of relevance 12 | from cmk.gui.i18n import _ 13 | from cmk.gui.plugins.metrics import metric_info 14 | 15 | metric_info["hellobakerylevel"] = { 16 | # Set a title, use _() to allow translations 17 | "title": _("Hello bakery! level"), 18 | # Set the unit: Percentage has clear borders 19 | "unit": "%", 20 | # Choose a color that isn't red/yellow/green 21 | "color": "15/a", 22 | } 23 | 24 | -------------------------------------------------------------------------------- /mkp/hellobakery/mkp/_web/plugins/perfometer/hellobakery_perfometer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Copyright (C) 2021 Mattias Schlenker for tribe29 GmbH 4 | # License: GNU General Public License v2 5 | # 6 | # Reference for details: 7 | # https://docs.checkmk.com/latest/en/devel_check_plugins.html#perfometer 8 | # 9 | # Configuration for a simple perf-o-meter that displays percentage values. 10 | 11 | from cmk.gui.plugins.metrics import perfometer_info 12 | 13 | # Just create the most simple perf-o-meter displaying only one linear value. 14 | # We use the variable "hellobakerylevel" as reference. Since output ranges from 0 15 | # to 100 we just use the full range. 16 | 17 | perfometer_info.append({ 18 | "type": "linear", 19 | "segments": ["hellobakerylevel"], 20 | "total": 100.0, 21 | }) 22 | 23 | -------------------------------------------------------------------------------- /mkp/hellobakery/mkp/_web/plugins/wato/hellobakery_bakery.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Copyright (C) 2021 Mattias Schlenker for tribe29 GmbH 4 | # License: GNU General Public License v2 5 | # 6 | # Reference for details: 7 | # https://docs.checkmk.com/latest/en/bakery_api.html#ruleset 8 | # 9 | # This is the Setup GUI for our "Hello bakery!" plugin. It defines the parameters that 10 | # can be defined using the GUI and that will eventually be written to the configuration 11 | # on the host running the agent. 12 | 13 | from cmk.gui.i18n import _ 14 | from cmk.gui.plugins.wato import ( 15 | HostRulespec, 16 | rulespec_registry, 17 | ) 18 | from cmk.gui.cee.plugins.wato.agent_bakery.rulespecs.utils import RulespecGroupMonitoringAgentsAgentPlugins 19 | from cmk.gui.valuespec import ( 20 | Age, 21 | Dictionary, 22 | TextAscii, 23 | ) 24 | 25 | def _valuespec_hello_bakery(): 26 | return Dictionary( 27 | title=_("Hello bakery! (Linux, Solaris, Windows)"), 28 | help=_("This will deploy my example plugin."), 29 | elements=[ 30 | ("user", TextAscii( 31 | title=_("User for example plugin"), 32 | allow_empty=False, 33 | )), 34 | ("content", TextAscii( 35 | title=_("The actual content"), 36 | allow_empty=False, 37 | )), 38 | ("interval", 39 | Age( 40 | title=_("Run asynchronously"), 41 | label=_("Interval for collecting data"), 42 | default_value=300, # default: 5 minutes 43 | )), 44 | ], 45 | optional_keys=["interval"], 46 | ) 47 | 48 | rulespec_registry.register( 49 | HostRulespec( 50 | group=RulespecGroupMonitoringAgentsAgentPlugins, 51 | name="agent_config:hello_bakery", 52 | valuespec=_valuespec_hello_bakery, 53 | )) 54 | -------------------------------------------------------------------------------- /mkp/hellobakery/mkp/_web/plugins/wato/hellobakery_parameters.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Copyright (C) 2021 Mattias Schlenker for tribe29 GmbH 4 | # License: GNU General Public License v2 5 | # 6 | # Reference for details: 7 | # https://docs.checkmk.com/latest/en/devel_check_plugins.html#thresholdinformation 8 | # 9 | # This file contains the configuration defaults on the Checkmk server side for our 10 | # "Hello bakery!" example, plus hooks to display and alter the thresholds for 11 | # CRIT and WARN in the Setup GUI. 12 | 13 | # Import localization: 14 | 15 | from cmk.gui.i18n import _ 16 | 17 | # Import the data structures the GUI uses to pass defaults: 18 | 19 | from cmk.gui.valuespec import ( 20 | Dictionary, 21 | Tuple, 22 | Percentage, 23 | ) 24 | from cmk.gui.plugins.wato import ( 25 | CheckParameterRulespecWithoutItem, 26 | rulespec_registry, 27 | RulespecGroupCheckParametersOperatingSystem, 28 | ) 29 | 30 | # Create a function that returns a tuple containing default thresholds, 31 | # The title may be an arbitary string, this serves three tasks: 32 | # 1. Define reasonable defaults 33 | # 2. Provide an entry field with proper range and label for the Setup 34 | # 3. Create a dictionary passed to the agent-based plugin, either using 35 | # defaults or the overriden values 36 | 37 | def _parameter_valuespec_hellobakery_levels(): 38 | return Dictionary( 39 | elements=[ 40 | ("levels", Tuple( 41 | title=_("Levels"), 42 | elements=[ 43 | Percentage( 44 | title=_("Warning at"), 45 | default_value=80.0, 46 | ), 47 | Percentage( 48 | title=_("Critical at"), 49 | default_value=90.0, 50 | ), 51 | ], 52 | )), 53 | ], 54 | # required_keys=['levels'], # There is only one value, so its required 55 | ) 56 | 57 | # Create a rulespec to be used in your agent-based plugin: 58 | # 59 | # CheckParameterRulespecWithoutItem - skip the item: 60 | # check_hello_bakery(params, section): 61 | # If more than one value is to be checked, you probably would use the item 62 | 63 | rulespec_registry.register( 64 | CheckParameterRulespecWithoutItem( 65 | # as defined as check_ruleset_name in your agent-based script on 66 | # Checkmk server side in share/check_mk/checks/: 67 | check_group_name = "hello_bakery", 68 | group = RulespecGroupCheckParametersOperatingSystem, 69 | match_type = "dict", 70 | # the function above to issue default parameters 71 | parameter_valuespec = _parameter_valuespec_hellobakery_levels, 72 | title=lambda: _("Morning mood for hello world"), 73 | )) 74 | -------------------------------------------------------------------------------- /mkp/hellobakery/mkp/hello_bakery-0.1.3.mkp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschlenker/checkmk-snippets/49abc3923b248651f4cb67b8d346c13f2691d622/mkp/hellobakery/mkp/hello_bakery-0.1.3.mkp -------------------------------------------------------------------------------- /mkp/hellobakery/mkp/hello_bakery-0.1.6.mkp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschlenker/checkmk-snippets/49abc3923b248651f4cb67b8d346c13f2691d622/mkp/hellobakery/mkp/hello_bakery-0.1.6.mkp -------------------------------------------------------------------------------- /mkp/hellobakery/mkp/hello_bakery-0.1.8.mkp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschlenker/checkmk-snippets/49abc3923b248651f4cb67b8d346c13f2691d622/mkp/hellobakery/mkp/hello_bakery-0.1.8.mkp -------------------------------------------------------------------------------- /mkp/hellobakery/mkp/info: -------------------------------------------------------------------------------- 1 | {'author': 'Mattias Schlenker', 2 | 'description': 'Hello world example with added plugin for bakery. First ' 3 | 'tinker around with my "Hello world!" MKP, then have a look at ' 4 | 'the added files to distribute agent check and configuration ' 5 | 'via the agent bakery. Checkmk Enterprise Editions only!\n', 6 | 'download_url': 'https://github.com/mschlenker/checkmk-snippets/tree/main/mkp/hellobakery', 7 | 'files': {'agent_based': ['hello_bakery.py'], 8 | 'agents': ['plugins/hello_bakery', 9 | 'windows/plugins/hello_bakery.cmd'], 10 | 'checkman': ['hello_bakery'], 11 | 'lib': ['check_mk/base/cee/plugins/bakery/hello_bakery.py', 12 | 'python3/cmk/base/cee/plugins/bakery/hello_bakery.py'], 13 | 'web': ['plugins/metrics/hellobakery_metric.py', 14 | 'plugins/perfometer/hellobakery_perfometer.py', 15 | 'plugins/wato/hellobakery_bakery.py', 16 | 'plugins/wato/hellobakery_parameters.py']}, 17 | 'name': 'hello_bakery', 18 | 'num_files': 10, 19 | 'title': 'Hello bakery!', 20 | 'version': '0.1.7', 21 | 'version.min_required': '2.0.0', 22 | 'version.packaged': '2.0.0p14', 23 | 'version.usable_until': '2.1.999'} -------------------------------------------------------------------------------- /mkp/hellobakery/mkp/info.json: -------------------------------------------------------------------------------- 1 | {"author": "Mattias Schlenker", "description": "Hello world example with added plugin for bakery. First tinker around with my \"Hello world!\" MKP, then have a look at the added files to distribute agent check and configuration via the agent bakery. Checkmk Enterprise Editions only!\n", "download_url": "https:\/\/github.com\/mschlenker\/checkmk-snippets\/tree\/main\/mkp\/hellobakery", "files": {"agent_based": ["hello_bakery.py"], "agents": ["plugins\/hello_bakery", "windows\/plugins\/hello_bakery.cmd"], "checkman": ["hello_bakery"], "lib": ["check_mk\/base\/cee\/plugins\/bakery\/hello_bakery.py", "python3\/cmk\/base\/cee\/plugins\/bakery\/hello_bakery.py"], "web": ["plugins\/metrics\/hellobakery_metric.py", "plugins\/perfometer\/hellobakery_perfometer.py", "plugins\/wato\/hellobakery_bakery.py", "plugins\/wato\/hellobakery_parameters.py"]}, "name": "hello_bakery", "title": "Hello bakery!", "version": "0.1.7", "version.min_required": "2.0.0", "version.packaged": "2.0.0p14", "version.usable_until": "2.1.999", "num_files": 10} -------------------------------------------------------------------------------- /mkp/helloco2/ao2ampel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Copyright (C) 2021 Mattias Schlenker for tribe29 GmbH 4 | # License: GNU General Public License v2 5 | # 6 | # Reference for details: 7 | # https://docs.checkmk.com/latest/en/devel_check_plugins.html 8 | # 9 | # This is the main CMK/server side script for a "Hello World!" Plugin 10 | 11 | from .agent_based_api.v1 import * 12 | 13 | # Most simple discovery function, basically just checks for the agents 14 | # output containing a section named exactly as the service: 15 | 16 | def discover_co2ampel(section): 17 | yield Service() 18 | 19 | # Thresholds are taken from the default parameters defined in 20 | # ~/local/share/check_mk/web/plugins/wato/helloworld_parameters.py 21 | # respectively overwritten by setup GUI settings. 22 | 23 | def check_co2ampel(params, section): 24 | for line in section: 25 | # Proving a Metric is the first step to get nice graphs, adding this line 26 | # already allows you to create custom graphs using "hellolevel" 27 | # 28 | # If you want to define a default graph, look here: 29 | # ~/local/share/check_mk/web/plugins/metrics/helloworld_metric.py 30 | # 31 | # Everyone loves perf-o-meters, so take the next step and build one! 32 | # 33 | # ~/local/share/check_mk/web/plugins/perfometer/helloworld_perfometer.py 34 | if (line[0] == "co2"): 35 | yield Metric(name="co2", value=int(line[1]), boundaries=(0, 10000)) 36 | # Two very simple transitions, both are not hardcoded. 37 | # Here we have to cast the string received to float. To cast, use a function 38 | # that fails early. CheckMK catches exceptions and changes state to UNKNOWN. 39 | # This is much better than casting the level of your bathtub to 0% allowing 40 | # more water to be poured in and not recognizing there might be a problem 41 | # that has to be investigated. 42 | if float(line[1]) > params["co2levels"][1]: 43 | yield Result(state=State.CRIT, summary="Open windows immediately!") 44 | return 45 | elif float(line[1]) > params["co2levels"][0]: 46 | yield Result(state=State.WARN, summary="Try to let in clean air!") 47 | return 48 | yield Result(state=State.OK, summary="The air is just fine.") 49 | 50 | # Register the plugin with a unique name and define at least three functions! 51 | # Default parameters can be left empty since defined elsewhere. 52 | 53 | register.check_plugin( 54 | # This is some unique identifier! Do not use dumb terms like "hello_world" or 55 | # "updates_pending" since someone else might have had your idea and your and his 56 | # or hers output is not parseable by your discovery/check function and vice versa! 57 | name = "co2ampel", 58 | # Nice human readable name. Be descriptive! 59 | service_name = "Watterott CO2 Ampel", 60 | # Refer to the discovery function above. 61 | discovery_function = discover_co2ampel, 62 | # Refer to the check function above. 63 | check_function = check_co2ampel, 64 | # Refer to the ruleset defined in: 65 | # ~/local/share/check_mk/web/plugins/wato/helloworld_parameters.py 66 | check_ruleset_name = "co2ampel", 67 | # check_default_parameters = {} 68 | # if defaults have to be defined here, use: 69 | check_default_parameters = { "levels" : (800, 1100) } 70 | ) 71 | -------------------------------------------------------------------------------- /mkp/helloco2/json_receiver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import urllib.request, json 4 | 5 | co2url = "http://10.76.23.147/json" 6 | 7 | print('<<>>') 8 | with urllib.request.urlopen(co2url) as url: 9 | data = json.loads(url.read().decode()) 10 | print('co2', data['c']) 11 | print('temp', data['t']) 12 | print('humidity', data['h']) 13 | print('lighting', data['l']) 14 | -------------------------------------------------------------------------------- /mkp/helloworld/README.md: -------------------------------------------------------------------------------- 1 | # Hello world! 2 | 3 | > [!WARNING] 4 | > This example uses an API that was replaced in Checkmk 2.3.0 and will be completely disabled in Checkmk 2.4.0. 5 | > Use this example only to compare with the new examples as a porting aid. 6 | > The new examples have been moved to [the Checkmk docs repo](https://github.com/Checkmk/checkmk-docs/tree/master/examples/bakery_api). 7 | > Furthermore "Hello world" and "Hello bakery" have been merged again. 8 | 9 | This is a very basic plugin that can be used as template for your own 10 | plugin development. It should be complete enough to fulfill the requirements 11 | of the Checkmk exchange. 12 | 13 | Besides this it can be used to create some noise for testing purposes. 14 | 15 | ## Contents 16 | 17 | A Python script for the agent side that outputs "hello_world" and a 18 | random number between 0.0 and 100.0, the CheckMK side sends WARN and 19 | CRIT when thresholds of 80.0 or 90.0 are reached (so expect a 20 | notification roughly every 5 minutes. 21 | 22 | ``` 23 | <<>> 24 | hello_world 90.24242012379389 25 | ``` 26 | 27 | The package includes: 28 | 29 | - Checkmk side agent based check 30 | - Configuration for editable thresholds 31 | - Definition of a graph 32 | - Definition of perf-o-meter 33 | 34 | -------------------------------------------------------------------------------- /mkp/helloworld/mkp/_agent_based/hello_world.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Copyright (C) 2021 Mattias Schlenker for tribe29 GmbH 4 | # License: GNU General Public License v2 5 | # 6 | # Reference for details: 7 | # https://docs.checkmk.com/latest/en/devel_check_plugins.html 8 | # 9 | # This is the main CMK/server side script for a "Hello World!" Plugin 10 | 11 | from .agent_based_api.v1 import * 12 | 13 | # Most simple discovery function, basically just checks for the agents 14 | # output containing a section named exactly as the service: 15 | 16 | def discover_hello_world(section): 17 | yield Service() 18 | 19 | # Thresholds are taken from the default parameters defined in 20 | # ~/local/share/check_mk/web/plugins/wato/helloworld_parameters.py 21 | # respectively overwritten by setup GUI settings. 22 | 23 | def check_hello_world(params, section): 24 | for line in section: 25 | # Proving a Metric is the first step to get nice graphs, adding this line 26 | # already allows you to create custom graphs using "hellolevel" 27 | # 28 | # If you want to define a default graph, look here: 29 | # ~/local/share/check_mk/web/plugins/metrics/helloworld_metric.py 30 | # 31 | # Everyone loves perf-o-meters, so take the next step and build one! 32 | # 33 | # ~/local/share/check_mk/web/plugins/perfometer/helloworld_perfometer.py 34 | yield Metric(name="hellolevel", value=float(line[1]), boundaries=(0.0, 100.0)) 35 | 36 | # Two very simple transitions, both are not hardcoded. 37 | # Here we have to cast the string received to float. To cast, use a function 38 | # that fails early. CheckMK catches exceptions and changes state to UNKNOWN. 39 | # This is much better than casting the level of your bathtub to 0% allowing 40 | # more water to be poured in and not recognizing there might be a problem 41 | # that has to be investigated. 42 | if float(line[1]) > params["levels"][1]: 43 | yield Result(state=State.CRIT, summary="Hello, leave me alone!") 44 | return 45 | elif float(line[1]) > params["levels"][0]: 46 | yield Result(state=State.WARN, summary="Hello, I need some coffee!") 47 | return 48 | yield Result(state=State.OK, summary="Hello World! What a lovely day.") 49 | 50 | # Register the plugin with a unique name and define at least three functions! 51 | # Default parameters can be left empty since defined elsewhere. 52 | 53 | register.check_plugin( 54 | # This is some unique identifier! Do not use dumb terms like "hello_world" or 55 | # "updates_pending" since someone else might have had your idea and your and his 56 | # or hers output is not parseable by your discovery/check function and vice versa! 57 | name = "hello_world", 58 | # Nice human readable name. Be descriptive! 59 | service_name = "Hello World!", 60 | # Refer to the discovery function above. 61 | discovery_function = discover_hello_world, 62 | # Refer to the check function above. 63 | check_function = check_hello_world, 64 | # Refer to the ruleset defined in: 65 | # ~/local/share/check_mk/web/plugins/wato/helloworld_parameters.py 66 | check_ruleset_name = "hello_world", 67 | # check_default_parameters = {} 68 | # if defaults have to be defined here, use: 69 | check_default_parameters = { "levels" : (80.0, 90.0) } 70 | ) 71 | 72 | -------------------------------------------------------------------------------- /mkp/helloworld/mkp/_agents/plugins/hello_world: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Copyright (C) 2021 Mattias Schlenker for tribe29 GmbH 4 | # License: GNU General Public License v2 5 | # 6 | # Reference for details: 7 | # https://docs.checkmk.com/latest/en/devel_check_plugins.html 8 | # 9 | # 10 | # This is the agent/client side script for a "Hello World!" Plugin for CheckMK 2.0 11 | # 12 | # /usr/lib/check_mk_agent/plugins/helloworld.py 13 | 14 | from random import random 15 | 16 | # The section should just use 7bit lowercase characters and underscores. They must 17 | # match the name set in register.check_plugin(). Make sure to choose a unique name 18 | # to avoid having two plugins with the same name. 19 | 20 | print("<<>>") 21 | 22 | # Just print a (fairly) random number between 0.0 and 100.0, interpreted as 23 | # percentage. The output will be space separated which will be split into tokens 24 | # as CheckMK receives the output. 25 | # 26 | # hello_world 98.41417513129443 27 | 28 | print("hello_world", random() * 100.0) 29 | 30 | -------------------------------------------------------------------------------- /mkp/helloworld/mkp/_checkman/hello_world: -------------------------------------------------------------------------------- 1 | title: Hello world sample 2 | agents: hello_world 3 | author: Mattias Schlenker 4 | license: GPLv2 5 | agents: linux, solaris, windows 6 | distribution: 7 | description: 8 | "Hello world!" is an example package primarily built for the purpose 9 | of showcasing minimal contents of a valid, usable package that can 10 | be uploaded to the CheckMK exchange. 11 | 12 | Expected agent output: 13 | 14 | <<>> 15 | 16 | hello_world 57.74685569821927 17 | 18 | The float is expected to be in the range between 0% and 100%. The 19 | sample agent relies on these four lines Python, store it as 20 | /usr/lib/check_mk_agent/plugins/helloworld and make sure it is 21 | set executable. 22 | 23 | Sample agent script (included) 24 | 25 | #!/usr/bin/env python3 26 | 27 | from random import random 28 | 29 | print("<<>>") 30 | 31 | print("hello_world", random() * 100.0) 32 | 33 | In default settings, above 80% "hellolevel" the state changes from 34 | OK -> WARNING, issueing the message "Hello, I need some coffee!", 35 | above 90%, the state will be CRIT "Hello, leave me alone!". 36 | 37 | Feel free to use the files contained in this MKP as a foundation for 38 | your first proper CheckMK plugin. 39 | 40 | -------------------------------------------------------------------------------- /mkp/helloworld/mkp/_web/plugins/metrics/helloworld_metric.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Copyright (C) 2021 Mattias Schlenker for tribe29 GmbH 4 | # License: GNU General Public License v2 5 | # 6 | # Reference for details: 7 | # https://docs.checkmk.com/latest/en/devel_check_plugins.html 8 | # 9 | # Here we define the metrics for the graph of our "Hello World!" Plugin 10 | 11 | # Import everything of relevance 12 | from cmk.gui.i18n import _ 13 | from cmk.gui.plugins.metrics import metric_info 14 | 15 | metric_info["hellolevel"] = { 16 | # Set a title, use _() to allow translations 17 | "title": _("Hello world level"), 18 | # Set the unit: Percentage has clear borders 19 | "unit": "%", 20 | # Choose a color that isn't red/yellow/green 21 | "color": "15/a", 22 | } 23 | 24 | -------------------------------------------------------------------------------- /mkp/helloworld/mkp/_web/plugins/perfometer/helloworld_perfometer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Copyright (C) 2021 Mattias Schlenker for tribe29 GmbH 4 | # License: GNU General Public License v2 5 | # 6 | # Reference for details: 7 | # https://docs.checkmk.com/latest/en/devel_check_plugins.html 8 | # 9 | # Configuration for a simple perf-o-meter that displays percentage 10 | 11 | from cmk.gui.plugins.metrics import perfometer_info 12 | 13 | 14 | # Just create the most simple perf-o-meter displaying only one linear value. 15 | # We use the variable "hellolevel" as reference. Since output ranges from 0 16 | # to 100 we just use the full range. 17 | 18 | perfometer_info.append({ 19 | "type": "linear", 20 | "segments": ["hellolevel"], 21 | "total": 100.0, 22 | }) 23 | 24 | -------------------------------------------------------------------------------- /mkp/helloworld/mkp/_web/plugins/wato/helloworld_parameters.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Copyright (C) 2021 Mattias Schlenker for tribe29 GmbH 4 | # License: GNU General Public License v2 5 | # 6 | # Reference for details: 7 | # https://docs.checkmk.com/latest/en/devel_check_plugins.html 8 | # 9 | # This file contains the configuration defaults for our "Hello world!" example 10 | # plus hooks to display and alter these defaults in the setup GUI. 11 | 12 | # Import localization: 13 | 14 | from cmk.gui.i18n import _ 15 | 16 | # Import the data structures the GUI uses to pass defaults: 17 | 18 | from cmk.gui.valuespec import ( 19 | Dictionary, 20 | Tuple, 21 | Percentage, 22 | ) 23 | from cmk.gui.plugins.wato import ( 24 | CheckParameterRulespecWithoutItem, 25 | rulespec_registry, 26 | RulespecGroupCheckParametersOperatingSystem, 27 | ) 28 | 29 | # Create a function that returns a tuple containing default thresholds, 30 | # the title may be an arbitary string, this serves three tasks: 31 | # 1. Define reasonable defaults 32 | # 2. Provide an entry field with proper range and label for the setup 33 | # 3. Create a dictionary passed to the agent based plugin, either using 34 | # defaults or the overriden values 35 | 36 | def _parameter_valuespec_helloworld_levels(): 37 | return Dictionary( 38 | elements=[ 39 | ("levels", Tuple( 40 | title=_("Levels"), 41 | elements=[ 42 | Percentage( 43 | title=_("Warning at"), 44 | default_value=80.0, 45 | ), 46 | Percentage( 47 | title=_("Critical at"), 48 | default_value=90.0, 49 | ), 50 | ], 51 | )), 52 | ], 53 | # required_keys=['levels'], # There is only one value, so its required 54 | ) 55 | 56 | # Create a rulespec to be used in your agent based plugin: 57 | # 58 | # CheckParameterRulespecWithoutItem - skip the item: 59 | # check_helloworld(params, section): 60 | # If more than one value is to be checked, you probably would use the item 61 | 62 | rulespec_registry.register( 63 | CheckParameterRulespecWithoutItem( 64 | # as defined in your check in share/check_mk/checks/ 65 | check_group_name = "hello_world", 66 | group = RulespecGroupCheckParametersOperatingSystem, 67 | match_type = "dict", 68 | # the function above to issue default parameters 69 | parameter_valuespec = _parameter_valuespec_helloworld_levels, 70 | title=lambda: _("Morning mood for hello world"), 71 | )) 72 | 73 | -------------------------------------------------------------------------------- /mkp/helloworld/mkp/hello_world-0.1.3.mkp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mschlenker/checkmk-snippets/49abc3923b248651f4cb67b8d346c13f2691d622/mkp/helloworld/mkp/hello_world-0.1.3.mkp -------------------------------------------------------------------------------- /mkp/helloworld/mkp/info: -------------------------------------------------------------------------------- 1 | {'author': 'Mattias Schlenker', 2 | 'description': 'This is a very basic plugin with the sole purpose to be used ' 3 | 'as template for your own plugin development: The Python files ' 4 | 'included make up for the most basic configurable plugin and ' 5 | 'are extensively commented. The structure should be complete ' 6 | 'enough to fulfill the requirements of the Checkmk exchange. ' 7 | 'If you install and use it without modification, it will just ' 8 | 'create random alerts.\n', 9 | 'download_url': 'https://github.com/mschlenker/checkmk-snippets/tree/main/mkp/helloworld', 10 | 'files': {'agent_based': ['hello_world.py'], 11 | 'agents': ['plugins/hello_world'], 12 | 'checkman': ['hello_world'], 13 | 'web': ['plugins/metrics/helloworld_metric.py', 14 | 'plugins/perfometer/helloworld_perfometer.py', 15 | 'plugins/wato/helloworld_parameters.py']}, 16 | 'name': 'hello_world', 17 | 'num_files': 6, 18 | 'title': 'Hello world!', 19 | 'version': '0.1.3', 20 | 'version.min_required': '2.0.0', 21 | 'version.packaged': '2.0.0p9', 22 | 'version.usable_until': '2.1.999'} -------------------------------------------------------------------------------- /mkp/helloworld/mkp/info.json: -------------------------------------------------------------------------------- 1 | {"author": "Mattias Schlenker", "description": "This is a very basic plugin with the sole purpose to be used as template for your own plugin development: The Python files included make up for the most basic configurable plugin and are extensively commented. The structure should be complete enough to fulfill the requirements of the Checkmk exchange. If you install and use it without modification, it will just create random alerts.\n", "download_url": "https:\/\/github.com\/mschlenker\/checkmk-snippets\/tree\/main\/mkp\/helloworld", "files": {"agent_based": ["hello_world.py"], "agents": ["plugins\/hello_world"], "checkman": ["hello_world"], "web": ["plugins\/metrics\/helloworld_metric.py", "plugins\/perfometer\/helloworld_perfometer.py", "plugins\/wato\/helloworld_parameters.py"]}, "name": "hello_world", "title": "Hello world!", "version": "0.1.3", "version.min_required": "2.0.0", "version.packaged": "2.0.0p9", "version.usable_until": "2.1.999", "num_files": 6} -------------------------------------------------------------------------------- /mkp/watterott_co2_ampel/mkp/_agent_based/co2welectronic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # © 2022 Mattias Schlenker for tribe29 GmbH 5 | # © 2023 Mattias Schlenker for Checkmk GmbH 6 | # 7 | # Background: The Watterott CO2 Ampel ("CO2 traffic light") is a networkable 8 | # sensor board, primarily made for monitoring CO2. It is open hardware and uses 9 | # an open source firmware. 10 | # 11 | # https://learn.watterott.com/breakouts/co2-ampel/ 12 | # 13 | # The factory firmware includes a simple Checkmk agent via HTTP/REST-API: 14 | # 15 | # http://12.34.56.78/cmk-agent 16 | # 17 | # <<>> 18 | # AgentOS: arduino 19 | # <<>> 20 | # co2 521 21 | # temp 19.3 22 | # humidity 51.8 23 | # lighting 976 24 | # temp2 19.4 25 | # pressure 1022.0 26 | # <<>> 27 | # P "CO2 level (ppm)" co2ppm=521;1000;1200 CO2/ventilation control with Watterott CO2-Ampel, thresholds taken from sensor board. 28 | # 29 | # Since the agent is only available via HTTP, the monitoring has to be configured 30 | # using "individual program call instead of agent access", see 31 | # 32 | # https://docs.checkmk.com/latest/en/datasource_programs.html 33 | # 34 | # The result looks like: 35 | # 36 | # curl http://$_HOSTADDRESS_4$/cmk-agent 37 | # 38 | # The local check creates a service immediately after discovery. However this 39 | # takes thresholds from the EPROM of the boards which makes central administration 40 | # difficult. This plugin adds discovery for all other sensors. Since different 41 | # versions of the board have differect sensors, individual discovery is needed. 42 | # 43 | # See ASR3.5 and ASR3.6 (Germany) for thresholds on CO2/temperature/humidity in 44 | # working environments. Only CO2 is quite fixed at 1000ppm. If no quick exchange 45 | # of air is possible, lower thresholds from 1000/1200 to 900/1000. 46 | 47 | from .agent_based_api.v1 import * 48 | from typing import Any, MutableMapping, Optional, Tuple 49 | 50 | def store_last_value(value_store: MutableMapping[str, Any], value, key: str) -> None: 51 | value_store[key] = value 52 | 53 | def discover_co2_level(section): 54 | for key, _value in section: 55 | yield Service(item=key) 56 | 57 | def check_co2_level(item, params, section): 58 | for key, value in section: 59 | # The Sensirion CO2 sensor 60 | if key == "co2" and key == item: 61 | yield Metric(name="co2", value=int(value), boundaries=(0, 10000), levels=params["co2"]) 62 | value_store = get_value_store() 63 | # Last state of service to compare 64 | last_co2 = 0 65 | if (last_co2 := value_store.get("co2")) is None: 66 | last_co2 = 0 67 | if int(value) > params["co2"][1]: 68 | store_last_value(value_store, 2, "co2") 69 | yield Result(state=State.CRIT, summary=f"CO2 level is too high at {value}ppm (threshold from plugin)") 70 | return 71 | # Factor in hysteresis: 72 | elif int(value) > int(params["co2"][1] * (1.0 - params["hysteresis"][0] / 100.0 )) and last_co2 > 1: 73 | store_last_value(value_store, 2, "co2") 74 | yield Result(state=State.CRIT, summary=f"CO2 level is too high at {value}ppm (threshold from plugin, hysteresis considered)") 75 | return 76 | elif int(value) > params["co2"][0]: 77 | store_last_value(value_store, 1, "co2") 78 | yield Result(state=State.WARN, summary=f"CO2 level is slightly too high at {value}ppm (threshold from plugin)") 79 | return 80 | # Factor in hysteresis: 81 | elif int(value) > int(params["co2"][0] * (1.0 - params["hysteresis"][0] / 100.0 )) and last_co2 > 0: 82 | store_last_value(value_store, 1, "co2") 83 | yield Result(state=State.WARN, summary=f"CO2 level is slightly too high at {value}ppm (threshold from plugin, hysteresis considered)") 84 | return 85 | store_last_value(value_store, 0, "co2") 86 | yield Result(state=State.OK, summary=f"CO2 level is acceptable at {value}ppm (threshold from plugin)") 87 | # Temperature senosr on the Sensirion 88 | elif key == "temp" and key == item: 89 | yield Metric(name="temp", value=float(value), boundaries=(-20.0, 80.0), levels=params["temp_upper"]) 90 | if float(value) > params["temp_upper"][1]: 91 | yield Result(state=State.CRIT, summary=f"Temperature is too high at {value}°C (threshold from plugin)") 92 | return 93 | elif float(value) > params["temp_upper"][0]: 94 | yield Result(state=State.WARN, summary=f"Temperature is slightly too high at {value}°C (threshold from plugin)") 95 | return 96 | elif float(value) < params["temp_lower"][1]: 97 | yield Result(state=State.CRIT, summary=f"Temperature is too low at {value}°C (threshold from plugin)") 98 | return 99 | elif float(value) < params["temp_lower"][0]: 100 | yield Result(state=State.WARN, summary=f"Temperature is slightly too low at {value}°C (threshold from plugin)") 101 | return 102 | yield Result(state=State.OK, summary=f"Temperature is acceptable at {value}°C (threshold from plugin)") 103 | # Boards with pressure sensors have a second temperature sensor as part of the pressure sensor 104 | elif key == "temp2" and key == item: 105 | yield Metric(name="temp_2", value=float(value), boundaries=(-20.0, 80.0), levels=params["temp_upper"]) 106 | if float(value) > params["temp_upper"][1]: 107 | yield Result(state=State.CRIT, summary=f"Temperature (sensor 2) is too high at {value}°C (threshold from plugin)") 108 | return 109 | elif float(value) > params["temp_upper"][0]: 110 | yield Result(state=State.WARN, summary=f"Temperature (sensor 2) is slightly too high at {value}°C (threshold from plugin)") 111 | return 112 | elif float(value) < params["temp_lower"][1]: 113 | yield Result(state=State.CRIT, summary=f"Temperature (sensor 2) is too low at {value}°C (threshold from plugin)") 114 | return 115 | elif float(value) < params["temp_lower"][0]: 116 | yield Result(state=State.WARN, summary=f"Temperature (sensor 2) is slightly too low at {value}°C (threshold from plugin)") 117 | return 118 | yield Result(state=State.OK, summary=f"Temperature (sensor 2) is acceptable at {value}°C (threshold from plugin)") 119 | # The humidity sensor 120 | elif key == "humidity" and key == item: 121 | yield Metric(name="humidity", value=float(value), levels=params["humidity_upper"]) 122 | if float(value) > params["humidity_upper"][1]: 123 | yield Result(state=State.CRIT, summary="Humidity is too humid at " + value + "% (threshold from plugin)") 124 | return 125 | elif float(value) > params["humidity_upper"][0]: 126 | yield Result(state=State.WARN, summary="Humidity is slightly too humid at " + value + "% (threshold from plugin)") 127 | return 128 | elif float(value) < params["humidity_lower"][1]: 129 | yield Result(state=State.CRIT, summary="Humidity is too dry at " + value + "% (threshold from plugin)") 130 | return 131 | elif float(value) < params["humidity_lower"][0]: 132 | yield Result(state=State.CRIT, summary="Humidity is slightly too dry at " + value + "% (threshold from plugin)") 133 | return 134 | yield Result(state=State.OK, summary="Humidity is acceptable at " + value + "% (threshold from plugin)") 135 | # For ambient lighting and pressure (if available) we just create services that are always OK 136 | elif key == item: 137 | yield Metric(name=key, value=float(value)) 138 | yield Result(state=State.OK, summary="Sensor " + key + " value " + value + " for informational purpose only, always OK") 139 | 140 | register.check_plugin( 141 | name = "watterott_co2ampel_plugin", 142 | service_name = "CO2 board %s", 143 | discovery_function = discover_co2_level, 144 | check_function = check_co2_level, 145 | check_ruleset_name = "watterott_co2ampel_plugin", 146 | # Define some thresholds, the CO2 values are taken according German Arbeitsstättenrichtlinie ASR3.6 147 | # Change temperatures for the respective work environmen, see ASR A3.5 148 | check_default_parameters = { 149 | "co2" : (1000, 1200), 150 | "temp_upper" : (23.0, 26.0), 151 | "temp_lower" : (17.0, 13.0), 152 | "humidity_upper" : (60.0, 65.0), 153 | "humidity_lower" : (35.0, 30.0), 154 | "hysteresis" : (5.0), 155 | } 156 | ) 157 | 158 | -------------------------------------------------------------------------------- /mkp/watterott_co2_ampel/mkp/_web/plugins/wato/co2welectronic_parameters.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # © 2022 Mattias Schlenker for tribe29 GmbH 5 | # © 2023 Mattias Schlenker for Checkmk GmbH 6 | # 7 | # Background: The Watterott CO2 Ampel ("CO2 traffic light") is a networkabale 8 | # sensor board, primarily made for monitoring CO2. It is open hardware and uses 9 | # an open source firmware. 10 | # 11 | # This GUI plugin creates the inputs for setting thresholds. Remember: 12 | # 13 | # 1. Both temperature sensors (if present) use the same thresholds (use 14 | # the most precise one if they are running apart) 15 | # 2. For temperature and humidity corridors can be applied, the graphing 16 | # system however only accepts upper boundaries 17 | # 3. Currently no thresholds for atmospheric pressure and ambient lighting 18 | # are used 19 | 20 | from cmk.gui.valuespec import ( 21 | Dictionary, 22 | Tuple, 23 | Percentage, 24 | Integer, 25 | Float, 26 | ) 27 | from cmk.gui.plugins.wato import ( 28 | CheckParameterRulespecWithoutItem, 29 | rulespec_registry, 30 | RulespecGroupCheckParametersApplications, 31 | RulespecGroupCheckParametersEnvironment, 32 | ) 33 | 34 | def _parameter_valuespec_co2_levels(): 35 | return Dictionary( 36 | elements=[ 37 | ("co2", Tuple( 38 | title=_("CO2 levels"), 39 | elements=[ 40 | Integer( 41 | title=_("Warning above"), 42 | default_value=1000, 43 | ), 44 | Integer( 45 | title=_("Critical above"), 46 | default_value=1200, 47 | ), 48 | ], 49 | )), 50 | ("temp_upper", Tuple( 51 | title=_("Temperature upper"), 52 | elements=[ 53 | Float( 54 | title=_("Warning above"), 55 | default_value=23.0, 56 | ), 57 | Float( 58 | title=_("Critical above"), 59 | default_value=26.0, 60 | ), 61 | ], 62 | )), 63 | ("temp_lower", Tuple( 64 | title=_("Temperature lower"), 65 | elements=[ 66 | Float( 67 | title=_("Warning below"), 68 | default_value=17.0, 69 | ), 70 | Float( 71 | title=_("Critical below"), 72 | default_value=13.0, 73 | ), 74 | ], 75 | )), 76 | ("humidity_upper", Tuple( 77 | title=_("Humidity upper"), 78 | elements=[ 79 | Percentage( 80 | title=_("Warning above"), 81 | default_value=60.0, 82 | ), 83 | Percentage( 84 | title=_("Critical above"), 85 | default_value=65.0, 86 | ), 87 | ], 88 | )), 89 | ("humidity_lower", Tuple( 90 | title=_("Humidity lower"), 91 | elements=[ 92 | Percentage( 93 | title=_("Warning below"), 94 | default_value=35.0, 95 | ), 96 | Percentage( 97 | title=_("Critical below"), 98 | default_value=30.0, 99 | ), 100 | ], 101 | )), 102 | ("hysteresis", Tuple( 103 | title=_("Hysteresis"), 104 | help=_("Specifying a hysteresis helps venting a bit longer and thus effectively prevents flapping. In general it is recommended to vent until CO2 reaches 600ppm. If this is not always possible, a hysteresis of 20% together with a WARN threshold of 1000ppm means that OK -> WARN takes place at 1000ppm, but WARN -> OK at 800ppm. Properly set, you may vent only by service state without watching out for blue light."), 105 | elements=[ 106 | Percentage( 107 | title=_("CO2 (percentage)"), 108 | default_value=5.0, 109 | ), 110 | # Float( 111 | # title=_("Temperature (absolute)"), 112 | # default_value=1.0, 113 | # ), 114 | ], 115 | )), 116 | ], 117 | ) 118 | 119 | rulespec_registry.register( 120 | CheckParameterRulespecWithoutItem( 121 | # as defined in your check in share/check_mk/checks/ 122 | check_group_name = "watterott_co2ampel_plugin", 123 | group = RulespecGroupCheckParametersEnvironment, 124 | match_type = "dict", 125 | # the function above to issue default parameters 126 | parameter_valuespec = _parameter_valuespec_co2_levels, 127 | title=lambda: _("Sensor levels from Watterott CO2 traffic light"), 128 | )) 129 | 130 | -------------------------------------------------------------------------------- /mkp/watterott_co2_ampel/mkp/info: -------------------------------------------------------------------------------- 1 | {'author': 'Mattias Schlenker ', 2 | 'description': 'Background: The Watterott CO2 Ampel ("CO2 traffic light") is ' 3 | 'a networkable sensor board, primarily made for monitoring ' 4 | 'CO2, but includes other sensors as temperature and humidity ' 5 | 'as well. It is open hardware and uses an open source ' 6 | 'firmware. https://learn.watterott.com/breakouts/co2-ampel/ \n' 7 | '\n' 8 | 'The factory firmware includes a simple Checkmk agent via ' 9 | 'HTTP/REST-API: http://12.34.56.78/cmk-agent\n' 10 | '\n' 11 | 'Since the agent is only available via HTTP, the monitoring ' 12 | 'has to be configured using "individual program call instead ' 13 | 'of agent access", see\n' 14 | '\n' 15 | 'https://docs.checkmk.com/latest/en/datasource_programs.html\n' 16 | '\n' 17 | 'The output includes one local check that creates a service ' 18 | 'immediately after discovery. However, this takes thresholds ' 19 | 'from the EPROM of the boards which makes central ' 20 | 'administration difficult. This plugin adds discovery for all ' 21 | 'other sensors. Since different versions of the board have ' 22 | 'different sensors, individual discovery is needed.\n' 23 | '\n' 24 | 'German users: See ASR3.5 and ASR3.6 (Germany) for thresholds ' 25 | 'on CO2/temperature/humidity in working environments. Only CO2 ' 26 | 'is quite fixed at 1000ppm. If no quick exchange of air is ' 27 | 'possible, lower thresholds from 1000/1200 to 900/1000 or ' 28 | 'properly adjust the hysteresis.\n', 29 | 'download_url': 'https://github.com/mschlenker/checkmk-snippets/', 30 | 'files': {'agent_based': ['co2welectronic.py'], 31 | 'web': ['plugins/wato/co2welectronic_parameters.py']}, 32 | 'name': 'watterott_CO2_ampel', 33 | 'title': 'Watterott CO2 Ampel', 34 | 'version': '0.2.0', 35 | 'version.min_required': '2.1.0b1', 36 | 'version.packaged': '2.2.0p2', 37 | 'version.usable_until': '2.2.99'} 38 | -------------------------------------------------------------------------------- /mkp/watterott_co2_ampel/mkp/info.json: -------------------------------------------------------------------------------- 1 | {"title": "Watterott CO2 Ampel", "name": "watterott_CO2_ampel", "description": "Background: The Watterott CO2 Ampel (\"CO2 traffic light\") is a networkable sensor board, primarily made for monitoring CO2, but includes other sensors as temperature and humidity as well. It is open hardware and uses an open source firmware. https:\/\/learn.watterott.com\/breakouts\/co2-ampel\/ \n\nThe factory firmware includes a simple Checkmk agent via HTTP\/REST-API: http:\/\/12.34.56.78\/cmk-agent\n\nSince the agent is only available via HTTP, the monitoring has to be configured using \"individual program call instead of agent access\", see\n\nhttps:\/\/docs.checkmk.com\/latest\/en\/datasource_programs.html\n\nThe output includes one local check that creates a service immediately after discovery. However, this takes thresholds from the EPROM of the boards which makes central administration difficult. This plugin adds discovery for all other sensors. Since different versions of the board have different sensors, individual discovery is needed.\n\nGerman users: See ASR3.5 and ASR3.6 (Germany) for thresholds on CO2\/temperature\/humidity in working environments. Only CO2 is quite fixed at 1000ppm. If no quick exchange of air is possible, lower thresholds from 1000\/1200 to 900\/1000 or properly adjust the hysteresis.\n", "version": "0.2.0", "version.packaged": "2.2.0p2", "version.min_required": "2.1.0b1", "version.usable_until": "2.2.99", "author": "Mattias Schlenker ", "download_url": "https:\/\/github.com\/mschlenker\/checkmk-snippets\/", "files": {"agent_based": ["co2welectronic.py"], "web": ["plugins\/wato\/co2welectronic_parameters.py"]}} -------------------------------------------------------------------------------- /mkp/watterott_co2_ampel/serial2spool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import time 4 | import json 5 | import serial 6 | import sys 7 | import getopt 8 | 9 | help = """ 10 | This program continously writes a Checkmk spool file for a Watterott CO2 traffic 11 | light that is connected to the USB port. The format is the same as for the REST 12 | endpoint on the WiFi enabled devices. You can change those parameters: 13 | 14 | -p optionally specify a host to create piggyback output for 15 | -o change path of outpt file (default /var/lib/check_mk_agent/spool/300_co2_ampel.txt) 16 | -s serial port to use (default /dev/ttyACM0) 17 | -i interval between writes (default 20s) 18 | -h this help 19 | 20 | See also: 21 | https://learn.watterott.com/breakouts/co2-ampel/ 22 | https://exchange.checkmk.com/p/watterott-co2-ampel 23 | https://docs.checkmk.com/latest/en/piggyback.html 24 | """ 25 | 26 | outfile = "/var/lib/check_mk_agent/spool/300_co2_ampel.txt" 27 | piggy = "" 28 | port = '/dev/ttyACM0' 29 | interval = 20 30 | 31 | co2 = 0 32 | temp = 0 33 | humidity = 0 34 | lighting = 0 35 | lastwrite = 0 36 | 37 | opts, args = getopt.getopt(sys.argv[1:],"p:o:s:i:h") 38 | for opt, arg in opts: 39 | if opt == '-p': 40 | piggy = arg 41 | elif opt == '-o': 42 | outfile = arg 43 | elif opt == '-s': 44 | port = arg 45 | elif opt == '-i': 46 | interval = int(arg) 47 | elif opt == '-h': 48 | print(help) 49 | sys.exit() 50 | 51 | ser = serial.Serial(port, 115200, timeout=3) 52 | newlines = 0 53 | 54 | while newlines < 1: 55 | data = ser.readline() 56 | if data.decode('utf-8').strip() == "": 57 | newlines += 1 58 | 59 | while True: 60 | data = ser.readline() 61 | if data.decode('utf-8').strip() == "" and int(time.time()) > lastwrite + interval: 62 | o = open(outfile, "w") 63 | if piggy != "": 64 | o.write("<<<<" + piggy + ">>>>\n") 65 | o.write("<<>>\n") 66 | o.write("co2 " + str(co2) + "\n") 67 | o.write("temp " + str(temp) + "\n") 68 | o.write("humidity " + str(humidity) + "\n") 69 | o.write("lighting " + str(lighting) + "\n") 70 | if piggy != "": 71 | o.write("<<<<>>>>\n") 72 | o.close() 73 | lastwrite = int(time.time()) 74 | else: 75 | toks = data.decode('utf-8').strip().split(": ", 1) 76 | if toks[0] == 'c': 77 | co2 = int(toks[1]) 78 | elif toks[0] == 't': 79 | temp = float(toks[1]) 80 | elif toks[0] == 'h': 81 | humidity = float(toks[1]) 82 | elif toks[0] == 'l': 83 | lighting = int(toks[1]) 84 | -------------------------------------------------------------------------------- /scripts/certcompare.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # encoding: utf-8 3 | # license: GPLv2 4 | # author: Mattias Schlenker for tribe29 GmbH 5 | 6 | # First parameter: old file 7 | # Second parameter: new file 8 | 9 | require 'openssl' 10 | 11 | oldfile = ARGV[0] 12 | newfile = ARGV[1] 13 | allfiles = [ oldfile, newfile ] 14 | inside = false 15 | # oldcerts and newcerts 16 | allcerts = [ [], [] ] 17 | # Serial numbers of certs to speed up comparison 18 | serials = [ [], [] ] 19 | raw = '' 20 | infos = [] 21 | warnings = [] 22 | errors = [] 23 | now = Time.now 24 | loglines = [] 25 | 26 | [ 0, 1].each { |n| 27 | File.open(allfiles[n]).each { |line| 28 | inside = true if line.strip == "-----BEGIN CERTIFICATE-----" 29 | raw = raw + line if inside == true 30 | if line.strip == "-----END CERTIFICATE-----" 31 | inside = false 32 | cert = OpenSSL::X509::Certificate.new raw 33 | allcerts[n].push cert 34 | raw = '' 35 | end 36 | } 37 | } 38 | 39 | [ 0, 1].each { |n| 40 | allcerts[n].each { |c| serials[n].push c.serial } 41 | } 42 | 43 | allcerts[1].each { |c| 44 | serial = c.serial 45 | unless serials[0].include? serial 46 | warnings.push c 47 | loglines.push "WARN: added certificate missing in old file." 48 | loglines.push " #{c.issuer}" 49 | loglines.push " serial: #{serial}" 50 | loglines.push " valid from: #{c.not_before}" 51 | loglines.push " valid til: #{c.not_after}" 52 | # loglines.push " fingerprint: #{c.fingerprint}" 53 | end 54 | } 55 | 56 | allcerts[0].each { |c| 57 | serial = c.serial 58 | if now > c.not_after 59 | # Notify on outdated certificates 60 | infos.push c 61 | loglines.push "NOTE: outdated certificate found!" 62 | loglines.push " #{c.issuer}" 63 | loglines.push " serial: #{serial}" 64 | loglines.push " valid til: #{c.not_after}" 65 | # loglines.push " fingerprint: #{c.fingerprint}" 66 | else 67 | # Try to find certificate in new certificate store 68 | unless serials[1].include? serial 69 | errors.push c 70 | loglines.push "ERROR: missing certificate, probably revoked!" 71 | loglines.push " #{c.issuer}" 72 | loglines.push " serial: #{serial}" 73 | loglines.push " valid from: #{c.not_before}" 74 | loglines.push " valid til: #{c.not_after}" 75 | # loglines.push " fingerprint: #{c.fingerprint}" 76 | end 77 | end 78 | } 79 | 80 | loglines.each { |l| puts l } 81 | if errors.size > 0 82 | puts "Overall state: CRIT" 83 | elsif warnings.size > 0 84 | puts "Overall state: WARN" 85 | else 86 | puts "Overall state: OK" 87 | end 88 | puts "Errors: #{errors.size}, warnings: #{warnings.size}, notes: #{infos.size}" 89 | -------------------------------------------------------------------------------- /scripts/fonttest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | 17 | The quick brown fox jumps over the lazy dog The quick brown fox jumps over the lazy dog The quick brown fox jumps over the lazy dog 18 | The quick brown fox jumps over the lazy dog The quick brown fox jumps over the lazy dog The quick brown fox jumps over the lazy dog 19 | The quick brown fox jumps over the lazy dog The quick brown fox jumps over the lazy dog The quick brown fox jumps over the lazy dog 20 | The quick brown fox jumps over the lazy dog The quick brown fox jumps over the lazy dog The quick brown fox jumps over the lazy dog 21 | The quick brown fox jumps over the lazy dog The quick brown fox jumps over the lazy dog The quick brown fox jumps over the lazy dog 22 | The quick brown fox jumps over the lazy dog. 23 | 24 | 25 | -------------------------------------------------------------------------------- /scripts/megax.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Start Firefox in a large dummy display to be able to get high resolution 4 | # screenshots. 5 | 6 | # Dependencies: 7 | # sudo apt-get install xfce4-terminal xterm rxvt-unicode x11vnc xvfb xfwm4 openbox 8 | 9 | # Other suggestions: xterm, xfce4-terminal 10 | TERMINAL=urxvt 11 | # Alternative suggestions: openbox, i3, fluxbox, twm, icewm 12 | WINMANAGER=xfwm4 13 | WIDTH=3840 14 | HEIGHT=3840 15 | DPI=96 16 | DPNUM=":7" 17 | FFPROFILE=CMK 18 | 19 | Xvfb ${DPNUM} -retro -nolisten tcp -dpi ${DPI} -screen ${DPNUM} ${WIDTH}x${HEIGHT}x24 & 20 | sleep 3 21 | 22 | DISPLAY=${DPNUM} ${TERMINAL} & 23 | DISPLAY=${DPNUM} firefox -P ${FFPROFILE} -no-remote & 24 | # Might be redundant, I don't care... 25 | DISPLAY=${DPNUM} x11vnc -localhost -loop -display ${DPNUM} & 26 | DISPLAY=${DPNUM} ${WINMANAGER} & 27 | 28 | # Start xfsettingsd here if needed. You can configure the appearance (icons, 29 | # theme, colors, fonts) with xfce4-settings-manager 30 | 31 | # Now use any usable VNC viewer to access the desktop. I suggest Remmina, 32 | # since this allows scaling. Connect to: 33 | # 34 | # localhost:0 35 | # 36 | # In rare occassions the display number might be different. 37 | # 38 | # To take screenshots you can run in any terminal: 39 | # 40 | # DISPLAY=:7 scrot /tmp/mycmkscreenshot.png 41 | # 42 | # Run in a terminal of its own. Or to kill: Close firefox, then first kill 43 | # x11vnc, then kill Xvfb. 44 | # 45 | # Have fun! Mattias -------------------------------------------------------------------------------- /scripts/rki_covid.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import requests 4 | import json 5 | import os.path 6 | import sys 7 | 8 | # File to store status info 9 | statfile = "/tmp/RKI_Data_Status.json" 10 | storedts = "" 11 | 12 | # Request the current status file 13 | staturl = 'https://opendata.arcgis.com/datasets/38e0356be30642868b4c526424102718_0.geojson' 14 | resp = requests.get(staturl) 15 | fromwhen = resp.json()['features'][0]['properties']['Timestamp_txt'] 16 | 17 | # Parameters of current data set for counties 18 | dataurl = 'https://opendata.arcgis.com/datasets/917fc37a709542548cc3be077a786c17_0.geojson' 19 | 20 | # Matching dumpfile 21 | datafile = "/tmp/RKI_Data_County.json" 22 | 23 | if os.path.isfile(statfile): 24 | # print("File exists.") 25 | compfile = json.load(open(statfile)) 26 | storedts = compfile['features'][0]['properties']['Timestamp_txt'] 27 | # print(compfile['features'][0]['properties']['Timestamp_txt']) 28 | 29 | # Anyway, dump the last status 30 | with open(statfile, 'w') as outfile: 31 | json.dump(resp.json(), outfile) 32 | 33 | # Compare the stored timestamp to the last received and retrieve the dataset if not present 34 | if storedts != fromwhen: 35 | dresp = requests.get(dataurl) 36 | with open(datafile, 'w') as outfile: 37 | json.dump(dresp.json(), outfile) 38 | 39 | # Read the actual data and display 40 | coviddata = json.load(open(datafile)) 41 | 42 | for (k, v) in coviddata.items(): 43 | if k == 'features': 44 | for (c) in v: 45 | if len(sys.argv) < 2: 46 | print(c['properties']['county']) 47 | elif sys.argv[1] == c['properties']['county']: 48 | print('<<>>') 49 | print('Version: 2.0.0') 50 | print('AgentOS: noOS') 51 | print('<<>>') 52 | # print('county ' + c['properties']['county']) 53 | print('cases7_per_100k ' + str(c['properties']['cases7_per_100k'])) 54 | print('state ' + str(c['properties']['BL'])) 55 | print('cases7_bl_per_100k ' + str(c['properties']['cases7_bl_per_100k'])) 56 | print('death7_lk ' + str(c['properties']['death7_lk'])) 57 | print('death7_bl ' + str(c['properties']['death7_bl'])) 58 | print('death_rate ' + str(c['properties']['death_rate'])) 59 | -------------------------------------------------------------------------------- /scripts/rki_covid_agbased.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from .agent_based_api.v1 import * 5 | 6 | def discover_rki_covid(section): 7 | yield Service() 8 | 9 | def check_rki_covid(params, section): 10 | for line in section: 11 | if line[0] != "state": 12 | yield Metric(name=line[0], value=float(line[1])) 13 | yield Result(state=State.OK, summary="Nothing is OK :-/") 14 | return 15 | 16 | register.check_plugin( 17 | name = "rki_covid19", 18 | service_name = "RKI COVID19 by county", 19 | check_function = check_rki_covid, 20 | discovery_function = discover_rki_covid, 21 | check_default_parameters = {} 22 | ) 23 | -------------------------------------------------------------------------------- /scripts/rki_covid_metrics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Import everything of relevance 5 | from cmk.gui.i18n import _ 6 | from cmk.gui.plugins.metrics import metric_info 7 | 8 | metric_info["cases7_per_100k"] = { 9 | "title": _("7 day incidence (county)"), 10 | "unit": "", 11 | "color": "15/a", 12 | } 13 | metric_info["cases7_bl_per_100k"] = { 14 | "title": _("7 day incidence (state)"), 15 | "unit": "", 16 | "color": "15/a", 17 | } 18 | metric_info["death7_lk"] = { 19 | "title": _("7 day deaths (county)"), 20 | "unit": "", 21 | "color": "15/a", 22 | } 23 | metric_info["death7_bl"] = { 24 | "title": _("7 day deaths (state)"), 25 | "unit": "", 26 | "color": "15/a", 27 | } 28 | metric_info["death_rate"] = { 29 | "title": _("Death rate"), 30 | "unit": "%", 31 | "color": "15/a", 32 | } 33 | --------------------------------------------------------------------------------